@simplysm/angular 14.2.1 → 14.2.3

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 (115) hide show
  1. package/dist/controls/button/sd-anchor.d.ts +1 -1
  2. package/dist/controls/button/sd-anchor.d.ts.map +1 -1
  3. package/dist/controls/button/sd-anchor.js +2 -2
  4. package/dist/controls/button/sd-button.d.ts +1 -1
  5. package/dist/controls/button/sd-button.d.ts.map +1 -1
  6. package/dist/controls/button/sd-button.js +2 -2
  7. package/dist/controls/checkbox/sd-checkbox.d.ts +1 -1
  8. package/dist/controls/checkbox/sd-checkbox.d.ts.map +1 -1
  9. package/dist/controls/checkbox/sd-checkbox.js +2 -2
  10. package/dist/controls/checkbox/sd-switch.d.ts +1 -1
  11. package/dist/controls/checkbox/sd-switch.d.ts.map +1 -1
  12. package/dist/controls/checkbox/sd-switch.js +2 -2
  13. package/dist/controls/dropdown/sd-dropdown.d.ts +3 -0
  14. package/dist/controls/dropdown/sd-dropdown.d.ts.map +1 -1
  15. package/dist/controls/dropdown/sd-dropdown.js +29 -9
  16. package/dist/controls/input/sd-textarea.d.ts +1 -1
  17. package/dist/controls/input/sd-textarea.d.ts.map +1 -1
  18. package/dist/controls/input/sd-textarea.js +2 -2
  19. package/dist/controls/input/sd-textfield-type-handlers.js +1 -1
  20. package/dist/controls/input/sd-textfield.d.ts +1 -1
  21. package/dist/controls/input/sd-textfield.d.ts.map +1 -1
  22. package/dist/controls/input/sd-textfield.js +9 -10
  23. package/dist/controls/list/sd-list.d.ts.map +1 -1
  24. package/dist/controls/list/sd-list.js +4 -6
  25. package/dist/controls/select/sd-select-button.d.ts.map +1 -1
  26. package/dist/controls/select/sd-select-button.js +3 -5
  27. package/dist/controls/select/sd-select.js +2 -2
  28. package/dist/core/error-handler/sd-global-error-handler.plugin.js +1 -1
  29. package/dist/core/modal/sd-modal.provider.js +1 -1
  30. package/dist/core/provideSdAngular.d.ts.map +1 -1
  31. package/dist/core/provideSdAngular.js +1 -13
  32. package/dist/core/setupBgTheme.d.ts +1 -1
  33. package/dist/core/setupBgTheme.d.ts.map +1 -1
  34. package/dist/core/toast/sd-toast.js +2 -2
  35. package/dist/core/toast/sd-toast.provider.d.ts +1 -1
  36. package/dist/core/toast/sd-toast.provider.d.ts.map +1 -1
  37. package/dist/core/toast/sd-toast.provider.js +1 -1
  38. package/dist/data/crud/sd-base-container.d.ts +1 -1
  39. package/dist/data/crud/sd-base-container.d.ts.map +1 -1
  40. package/dist/data/crud/sd-base-container.js +2 -2
  41. package/dist/data/crud/sd-crud-detail.d.ts +1 -1
  42. package/dist/data/crud/sd-crud-detail.d.ts.map +1 -1
  43. package/dist/data/crud/sd-crud-list.d.ts +3 -3
  44. package/dist/data/crud/sd-crud-list.d.ts.map +1 -1
  45. package/dist/data/shared-data/sd-shared-data-select.d.ts +2 -2
  46. package/dist/data/shared-data/sd-shared-data-select.d.ts.map +1 -1
  47. package/dist/data/sheet/injectSheetDomAccessor.d.ts +1 -0
  48. package/dist/data/sheet/injectSheetDomAccessor.d.ts.map +1 -1
  49. package/dist/data/sheet/injectSheetDomAccessor.js +7 -0
  50. package/dist/data/sheet/injectSheetDragSelection.d.ts +18 -0
  51. package/dist/data/sheet/injectSheetDragSelection.d.ts.map +1 -0
  52. package/dist/data/sheet/injectSheetDragSelection.js +193 -0
  53. package/dist/data/sheet/sd-sheet.d.ts +3 -0
  54. package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
  55. package/dist/data/sheet/sd-sheet.js +17 -6
  56. package/dist/data/state-preset/sd-state-preset.d.ts +2 -2
  57. package/dist/data/state-preset/sd-state-preset.d.ts.map +1 -1
  58. package/dist/features/editor/sd-tiptap-editor.js +2 -2
  59. package/dist/features/theme/sd-theme-provider.d.ts +0 -2
  60. package/dist/features/theme/sd-theme-provider.d.ts.map +1 -1
  61. package/dist/features/theme/sd-theme-provider.js +0 -5
  62. package/dist/features/theme/sd-theme-selector.d.ts +1 -1
  63. package/dist/features/theme/sd-theme-selector.d.ts.map +1 -1
  64. package/dist/features/theme/sd-theme-selector.js +5 -22
  65. package/dist/features/visual/sd-label.d.ts +1 -1
  66. package/dist/features/visual/sd-label.d.ts.map +1 -1
  67. package/dist/features/visual/sd-label.js +2 -2
  68. package/dist/features/visual/sd-note.d.ts +1 -1
  69. package/dist/features/visual/sd-note.d.ts.map +1 -1
  70. package/dist/features/visual/sd-note.js +2 -2
  71. package/dist/features/visual/sd-progress.d.ts +1 -1
  72. package/dist/features/visual/sd-progress.d.ts.map +1 -1
  73. package/dist/features/visual/sd-progress.js +2 -2
  74. package/dist/layout/sidebar/sd-sidebar.d.ts.map +1 -1
  75. package/dist/layout/sidebar/sd-sidebar.js +3 -5
  76. package/dist/layout/topbar/sd-topbar.d.ts.map +1 -1
  77. package/dist/layout/topbar/sd-topbar.js +3 -3
  78. package/package.json +26 -26
  79. package/scss/commons/_mixins.scss +1 -1
  80. package/scss/commons/_theme-variables.scss +0 -8
  81. package/scss/commons/_variables.scss +114 -121
  82. package/scss/themes/_variables-blueprint.scss +65 -63
  83. package/scss/themes/_variables-ide-dark.scss +166 -144
  84. package/src/controls/button/sd-anchor.ts +3 -3
  85. package/src/controls/button/sd-button.ts +1 -3
  86. package/src/controls/checkbox/sd-checkbox.ts +2 -10
  87. package/src/controls/checkbox/sd-switch.ts +2 -4
  88. package/src/controls/dropdown/sd-dropdown.ts +33 -9
  89. package/src/controls/input/sd-textarea.ts +2 -4
  90. package/src/controls/input/sd-textfield-type-handlers.ts +1 -1
  91. package/src/controls/input/sd-textfield.ts +9 -12
  92. package/src/controls/list/sd-list.ts +3 -5
  93. package/src/controls/select/sd-select-button.ts +2 -4
  94. package/src/controls/select/sd-select.ts +1 -1
  95. package/src/core/error-handler/sd-global-error-handler.plugin.ts +1 -1
  96. package/src/core/modal/sd-modal.provider.ts +1 -1
  97. package/src/core/provideSdAngular.ts +1 -15
  98. package/src/core/setupBgTheme.ts +1 -2
  99. package/src/core/toast/sd-toast.provider.ts +5 -3
  100. package/src/data/crud/sd-base-container.ts +1 -1
  101. package/src/data/sheet/injectSheetDomAccessor.ts +7 -0
  102. package/src/data/sheet/injectSheetDragSelection.ts +226 -0
  103. package/src/data/sheet/sd-sheet.ts +16 -3
  104. package/src/features/editor/sd-tiptap-editor.ts +1 -1
  105. package/src/features/theme/sd-theme-provider.ts +0 -9
  106. package/src/features/theme/sd-theme-selector.ts +1 -10
  107. package/src/features/visual/sd-label.ts +1 -3
  108. package/src/features/visual/sd-note.ts +1 -3
  109. package/src/features/visual/sd-progress.ts +2 -2
  110. package/src/layout/sidebar/sd-sidebar.ts +2 -3
  111. package/src/layout/topbar/sd-topbar.ts +1 -0
  112. package/styles.css +278 -567
  113. package/scss/commons/_colors.scss +0 -301
  114. package/scss/commons/_density.scss +0 -23
  115. package/scss/sd-tokens.md +0 -132
@@ -12,16 +12,14 @@ import {
12
12
  encapsulation: ViewEncapsulation.None,
13
13
  standalone: true,
14
14
  imports: [],
15
- template: `
16
- <ng-content />
17
- `,
15
+ template: ` <ng-content /> `,
18
16
  styles: [
19
17
  /* language=SCSS */ `
20
18
  sd-list {
21
19
  display: flex;
22
20
  flex-direction: column;
23
21
  user-select: none;
24
- background-color: var(--sd-bg-control);
22
+ background-color: var(--sd-bg-content);
25
23
  border: 1px solid var(--sd-bd-strong);
26
24
  border-radius: var(--sd-radius-default);
27
25
  padding: var(--sd-gap-sm);
@@ -36,7 +34,7 @@ import {
36
34
  }
37
35
  }
38
36
 
39
- // 아이템 내부에 중첩된 리스트는 카드 외형(테두리·라운드·배경)을 제거
37
+ // 아이템 내부에 중첩된 리스트는 카드 외형(테두리, 라운드, 배경)을 제거
40
38
  sd-list-item sd-list {
41
39
  border-color: transparent;
42
40
  border-radius: 0;
@@ -7,14 +7,12 @@ import { setupRipple } from "../../core/ripple/setupRipple";
7
7
  encapsulation: ViewEncapsulation.None,
8
8
  standalone: true,
9
9
  imports: [],
10
- template: `
11
- <ng-content />
12
- `,
10
+ template: ` <ng-content /> `,
13
11
  styles: [
14
12
  /* language=SCSS */ `
15
13
  sd-select-button {
16
14
  display: block;
17
- background-color: var(--sd-bg-control);
15
+ background-color: var(--sd-bg-content);
18
16
  font-weight: bold;
19
17
  cursor: pointer;
20
18
  color: var(--sd-tx-primary);
@@ -231,7 +231,7 @@ export type SelectModeValue<T> = {
231
231
 
232
232
  // inset(시트 셀 등)의 disabled 는 일반 콘텐츠처럼 표시(현행 유지)
233
233
  &[data-sd-disabled="true"] > sd-dropdown {
234
- background-color: var(--sd-bg-control);
234
+ background-color: var(--sd-bg-content);
235
235
 
236
236
  > ._sd-select-control {
237
237
  color: var(--sd-tx-default);
@@ -21,7 +21,7 @@ export class SdGlobalErrorHandlerPlugin implements ErrorHandler {
21
21
  private _hasDisplayedError = false;
22
22
 
23
23
  handleError(event: unknown) {
24
- // SSR(프리렌더) 가드: 서버에는 화면 오버레이·브라우저 전역 이벤트 타입이 없음 — 로그만 남김
24
+ // SSR(프리렌더) 가드: 서버에는 화면 오버레이, 브라우저 전역 이벤트 타입이 없음 — 로그만 남김
25
25
  if (!this._isBrowser) {
26
26
  logger.error(event);
27
27
  return false;
@@ -226,7 +226,7 @@ export class SdModalProvider {
226
226
  // transition duration 확인 후 대기 또는 즉시 destroy
227
227
  const duration = parseFloat(getComputedStyle(modalEl).transitionDuration || "0");
228
228
  if (duration > 0) {
229
- // transitionend 가 정상 경로. 단 백그라운드 탭·transition 인터럽트 시 미발화하므로
229
+ // transitionend 가 정상 경로. 단 백그라운드 탭, transition 인터럽트 시 미발화하므로
230
230
  // duration(초→ms) + 여유 100ms 후 발화하는 setTimeout 폴백을 함께 걸어 destroy 누락을 방지.
231
231
  modalEl.addEventListener("transitionend", doDestroy, { once: true });
232
232
  setTimeout(doDestroy, duration * 1000 + 100);
@@ -49,7 +49,7 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
49
49
  strokeWidth: 1.5,
50
50
  }),
51
51
  provideEnvironmentInitializer(() => {
52
- // SSR(프리렌더) 가드: 테마 저장·복원은 브라우저 전용
52
+ // SSR(프리렌더) 가드: 테마 저장, 복원은 브라우저 전용
53
53
  if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
54
54
 
55
55
  const sdTheme = inject(SdThemeProvider);
@@ -77,20 +77,6 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
77
77
  }
78
78
  });
79
79
 
80
- const savedDensity = sdLocalStorage.get("sd-theme-density");
81
- if (savedDensity === "compact" || savedDensity === "normal") {
82
- sdTheme.density.set(savedDensity);
83
- }
84
-
85
- let prevDensity = sdTheme.density();
86
- effect(() => {
87
- const density = sdTheme.density();
88
- if (density !== prevDensity) {
89
- sdLocalStorage.set("sd-theme-density", density);
90
- prevDensity = density;
91
- }
92
- });
93
-
94
80
  const savedFontSize = sdLocalStorage.get("sd-theme-font-size");
95
81
  if (savedFontSize != null) {
96
82
  sdTheme.fontSize.set(savedFontSize);
@@ -2,8 +2,7 @@ import { effect, inject, PLATFORM_ID } from "@angular/core";
2
2
  import { isPlatformBrowser } from "@angular/common";
3
3
 
4
4
  export function setupBgTheme(options?: {
5
- theme?:
6
- "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray";
5
+ theme?: "primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray";
7
6
  }): void {
8
7
  // SSR(프리렌더) 가드: document 반영은 브라우저 전용
9
8
  if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
@@ -20,7 +20,7 @@ import { SdSystemLogProvider } from "../config/sd-system-log.provider";
20
20
  import "@simplysm/core-browser";
21
21
 
22
22
  export type SdToastSeverity = "info" | "success" | "warning" | "danger";
23
- export type SdToastTheme = "primary" | "secondary" | SdToastSeverity | "gray" | "blue-gray";
23
+ export type SdToastTheme = "primary" | SdToastSeverity | "gray" | "blue-gray";
24
24
 
25
25
  export interface SdToastContentDef<O> {
26
26
  close: OutputEmitterRef<O | undefined>;
@@ -91,7 +91,9 @@ export class SdToastProvider {
91
91
  return this._show("danger", message, useProgress);
92
92
  }
93
93
 
94
- notify<T extends SdToastContentDef<any>>(input: SdToastInput<T>): Promise<Parameters<T["close"]["emit"]>[0] | undefined> {
94
+ notify<T extends SdToastContentDef<any>>(
95
+ input: SdToastInput<T>,
96
+ ): Promise<Parameters<T["close"]["emit"]>[0] | undefined> {
95
97
  return new Promise((resolve) => {
96
98
  // overlap 모드: 기존 토스트 모두 제거
97
99
  if (this.overlap()) {
@@ -241,7 +243,7 @@ export class SdToastProvider {
241
243
  let dismissPending = false;
242
244
  let dismissed = false;
243
245
 
244
- // 타이머·리스너 정리. 외부 파괴 경로(_dismissToast/_destroyToast)에서도 호출되어
246
+ // 타이머, 리스너 정리. 외부 파괴 경로(_dismissToast/_destroyToast)에서도 호출되어
245
247
  // 죽은 toastRef 에 대해 타이머가 깨어나는 race 와 리스너 누수를 막는다.
246
248
  const cleanup = () => {
247
249
  dismissed = true;
@@ -55,7 +55,7 @@ import { NgTemplateOutlet } from "@angular/common";
55
55
  </sd-busy-container>
56
56
 
57
57
  <ng-template #content>
58
- <div class="flex-column fill bg-control">
58
+ <div class="flex-column fill bg-content">
59
59
  @if (commandTplRef()) {
60
60
  <div class="p-sm-default flex-row gap-default bdb bdb-soft">
61
61
  <ng-template [ngTemplateOutlet]="commandTplRef()" />
@@ -51,6 +51,12 @@ export function injectSheetDomAccessor() {
51
51
  return tbody.querySelector<HTMLTableRowElement>(`tr[data-r='${r}']`);
52
52
  }
53
53
 
54
+ function getRows(): HTMLTableRowElement[] {
55
+ const tbody = getTable().querySelector("tbody");
56
+ if (tbody == null) return [];
57
+ return Array.from(tbody.querySelectorAll<HTMLTableRowElement>("tr[data-r]"));
58
+ }
59
+
54
60
  function getCell(r: number, c: number): HTMLTableCellElement | null {
55
61
  const row = getRow(r);
56
62
  if (row == null) return null;
@@ -69,6 +75,7 @@ export function injectSheetDomAccessor() {
69
75
  getSelectRowIndicators,
70
76
  getColumnResizeIndicator,
71
77
  getRow,
78
+ getRows,
72
79
  getCell,
73
80
  };
74
81
  }
@@ -0,0 +1,226 @@
1
+ import { DestroyRef, inject, type Signal } from "@angular/core";
2
+
3
+ export function injectSheetDragSelection<TItem>(options: {
4
+ domAccessor: {
5
+ getRows(): HTMLTableRowElement[];
6
+ getContainer(): HTMLElement;
7
+ getTHead(): HTMLTableSectionElement;
8
+ };
9
+ displayItems: Signal<TItem[]>;
10
+ selection: {
11
+ isSelected(item: TItem): boolean;
12
+ getSelectable(item: TItem): true | string | undefined;
13
+ select(item: TItem): void;
14
+ deselect(item: TItem): void;
15
+ };
16
+ }) {
17
+ const destroyRef = inject(DestroyRef);
18
+
19
+ let dragCleanup: (() => void) | null = null;
20
+
21
+ destroyRef.onDestroy(() => {
22
+ dragCleanup?.();
23
+ });
24
+
25
+ // 벗어난 1px 당 초당 스크롤 px, 그리고 초당 스크롤 상한
26
+ const AUTO_SCROLL_SPEED_PER_PX = 8;
27
+ const AUTO_SCROLL_MAX_SPEED = 2000;
28
+
29
+ // 고정 헤더에 가려지거나 컨테이너 밖에 있는 행은 소비자에게 안 보인다
30
+ function visibleBand(): { top: number; bottom: number } {
31
+ const containerRect = options.domAccessor.getContainer().getBoundingClientRect();
32
+ return {
33
+ top: containerRect.top + options.domAccessor.getTHead().offsetHeight,
34
+ bottom: containerRect.bottom,
35
+ };
36
+ }
37
+
38
+ function rowIndexOf(rowEl: HTMLTableRowElement): number {
39
+ const attr = rowEl.getAttribute("data-r");
40
+ const rowIdx = attr == null ? Number.NaN : parseInt(attr, 10);
41
+ if (Number.isNaN(rowIdx)) {
42
+ throw new Error(`시트 행 인덱스를 읽을 수 없습니다: data-r=${attr}`);
43
+ }
44
+ return rowIdx;
45
+ }
46
+
47
+ // 보이는 행 중 clientY 가 속한 행. 위아래로 벗어나면 가장 가까운 끝 행으로 맞춘다
48
+ function resolveRowIndex(clientY: number): number | null {
49
+ const band = visibleBand();
50
+ const visibleRows = options.domAccessor.getRows().filter((rowEl) => {
51
+ const rect = rowEl.getBoundingClientRect();
52
+ return rect.bottom > band.top && rect.top < band.bottom;
53
+ });
54
+ if (visibleRows.length === 0) return null;
55
+
56
+ const firstRect = visibleRows[0].getBoundingClientRect();
57
+ const lastRect = visibleRows[visibleRows.length - 1].getBoundingClientRect();
58
+ if (clientY < Math.max(firstRect.top, band.top)) return rowIndexOf(visibleRows[0]);
59
+ if (clientY >= Math.min(lastRect.bottom, band.bottom)) {
60
+ return rowIndexOf(visibleRows[visibleRows.length - 1]);
61
+ }
62
+
63
+ for (const rowEl of visibleRows) {
64
+ const rect = rowEl.getBoundingClientRect();
65
+ if (clientY < rect.top || clientY >= rect.bottom) continue;
66
+ return rowIndexOf(rowEl);
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // 가장자리를 넘어선 거리. 위로 벗어나면 음수, 아래로 벗어나면 양수
72
+ function overshootOf(clientY: number): number {
73
+ const band = visibleBand();
74
+ if (clientY < band.top) return clientY - band.top;
75
+ if (clientY >= band.bottom) return clientY - band.bottom;
76
+ return 0;
77
+ }
78
+
79
+ function onSelectorPointerDown(event: PointerEvent, r: number): void {
80
+ if (event.button !== 0) return;
81
+ if (event.shiftKey) return;
82
+ if (event.pointerType === "touch") return;
83
+
84
+ dragCleanup?.();
85
+
86
+ const startItem = options.displayItems()[r];
87
+ if (startItem == null) return;
88
+
89
+ // 시작 행을 누른 결과 상태가 곧 페인트 목표. 체크박스 자체 토글보다 먼저 스냅샷해야 뒤집히지 않는다
90
+ const isSelectTarget = !options.selection.isSelected(startItem);
91
+ const startEl = event.currentTarget;
92
+ let dragged = false;
93
+
94
+ // 칠하기 전 상태. 드래그를 되돌려 범위를 벗어난 행은 이 값으로 복원한다
95
+ const stateBeforePaint = new Map<number, boolean>();
96
+
97
+ const setSelected = (rowIdx: number, selected: boolean): void => {
98
+ const item = options.displayItems()[rowIdx];
99
+ if (item == null) return;
100
+ if (options.selection.getSelectable(item) !== true) return;
101
+
102
+ if (selected) {
103
+ options.selection.select(item);
104
+ } else {
105
+ options.selection.deselect(item);
106
+ }
107
+ };
108
+
109
+ const paint = (rowIdx: number): void => {
110
+ const item = options.displayItems()[rowIdx];
111
+ if (item == null) return;
112
+ if (options.selection.getSelectable(item) !== true) return;
113
+
114
+ if (!stateBeforePaint.has(rowIdx)) {
115
+ stateBeforePaint.set(rowIdx, options.selection.isSelected(item));
116
+ }
117
+ setSelected(rowIdx, isSelectTarget);
118
+ };
119
+
120
+ // 시작 행부터 현재 행까지가 칠할 범위. 범위를 벗어난 행은 원래대로 되돌린다
121
+ const applyRange = (currentRow: number): void => {
122
+ const from = Math.min(r, currentRow);
123
+ const to = Math.max(r, currentRow);
124
+
125
+ for (const rowIdx of [...stateBeforePaint.keys()]) {
126
+ if (rowIdx >= from && rowIdx <= to) continue;
127
+ setSelected(rowIdx, stateBeforePaint.get(rowIdx)!);
128
+ stateBeforePaint.delete(rowIdx);
129
+ }
130
+
131
+ for (let i = from; i <= to; i++) {
132
+ paint(i);
133
+ }
134
+ };
135
+
136
+ const applyRangeAt = (clientY: number): void => {
137
+ const rowIdx = resolveRowIndex(clientY);
138
+ if (rowIdx == null) return;
139
+ if (rowIdx === r && !dragged) return;
140
+
141
+ dragged = true;
142
+ applyRange(rowIdx);
143
+ };
144
+
145
+ // 가장자리를 넘긴 동안 벗어난 거리에 비례해 스크롤하며 계속 칠한다
146
+ let pointerY = event.clientY;
147
+ let rafId: number | null = null;
148
+ let lastFrameTime: number | null = null;
149
+
150
+ const autoScrollFrame = (time: number): void => {
151
+ const overshoot = overshootOf(pointerY);
152
+ if (overshoot === 0) {
153
+ rafId = null;
154
+ lastFrameTime = null;
155
+ return;
156
+ }
157
+
158
+ const elapsedSec = lastFrameTime == null ? 0 : (time - lastFrameTime) / 1000;
159
+ lastFrameTime = time;
160
+
161
+ const speed =
162
+ Math.sign(overshoot) *
163
+ Math.min(Math.abs(overshoot) * AUTO_SCROLL_SPEED_PER_PX, AUTO_SCROLL_MAX_SPEED);
164
+ options.domAccessor.getContainer().scrollTop += speed * elapsedSec;
165
+ applyRangeAt(pointerY);
166
+
167
+ rafId = requestAnimationFrame(autoScrollFrame);
168
+ };
169
+
170
+ const onPointerMove = (e: PointerEvent): void => {
171
+ pointerY = e.clientY;
172
+ applyRangeAt(pointerY);
173
+
174
+ if (overshootOf(pointerY) !== 0 && rafId == null) {
175
+ rafId = requestAnimationFrame(autoScrollFrame);
176
+ }
177
+ };
178
+
179
+ const onPointerUp = (): void => {
180
+ dragCleanup?.();
181
+ if (!dragged) return;
182
+
183
+ // 시작 행은 이미 페인트로 확정됐으므로 SdCheckbox 자체 토글이 되돌리지 못하게 막는다.
184
+ // capture 로 걸어야 SdCheckbox 의 host (click) 보다 먼저 실행된다
185
+ if (!(startEl instanceof HTMLElement)) return;
186
+
187
+ const blockClick = (e: Event): void => {
188
+ e.preventDefault();
189
+ e.stopImmediatePropagation();
190
+ };
191
+ startEl.addEventListener("click", blockClick, { once: true, capture: true });
192
+
193
+ // 다른 행에서 뗀 드래그는 시작 체크박스에 click 이 오지 않는다.
194
+ // 남겨두면 이후의 정상 클릭 1회를 삼키므로 이번 턴이 지나면 걷어낸다
195
+ setTimeout(() => {
196
+ startEl.removeEventListener("click", blockClick, { capture: true });
197
+ });
198
+ };
199
+
200
+ // 드래그 중 브라우저 텍스트 선택 억제.
201
+ // pointerdown 을 preventDefault 하면 클릭 포커스까지 사라져 shift 범위 선택의 기준 행이 깨지므로,
202
+ // 선택 시작 자체만 막는다
203
+ const blockSelectStart = (e: Event): void => {
204
+ e.preventDefault();
205
+ };
206
+ document.addEventListener("selectstart", blockSelectStart);
207
+
208
+ document.addEventListener("pointermove", onPointerMove);
209
+ document.addEventListener("pointerup", onPointerUp);
210
+ // 브라우저발 강제 종료(창 전환, 시스템 제스처)도 뗀 것과 동일하게 확정 처리
211
+ document.addEventListener("pointercancel", onPointerUp);
212
+ dragCleanup = () => {
213
+ if (rafId != null) {
214
+ cancelAnimationFrame(rafId);
215
+ rafId = null;
216
+ }
217
+ document.removeEventListener("selectstart", blockSelectStart);
218
+ document.removeEventListener("pointermove", onPointerMove);
219
+ document.removeEventListener("pointerup", onPointerUp);
220
+ document.removeEventListener("pointercancel", onPointerUp);
221
+ dragCleanup = null;
222
+ };
223
+ }
224
+
225
+ return { onSelectorPointerDown };
226
+ }
@@ -46,6 +46,7 @@ import { useSheetDisplayPipeline } from "./useSheetDisplayPipeline";
46
46
  import { useSheetCellStyling } from "./useSheetCellStyling";
47
47
  import { useSheetFocusIndicator } from "./useSheetFocusIndicator";
48
48
  import { injectSheetSelectRowIndicator } from "./injectSheetSelectRowIndicator";
49
+ import { injectSheetDragSelection } from "./injectSheetDragSelection";
49
50
  import { SdModalProvider } from "../../core/modal/sd-modal.provider";
50
51
  import { SdSheetConfigModal } from "./sd-sheet-config.modal";
51
52
  import { SdEvents } from "../../core/events/sd-events";
@@ -68,7 +69,7 @@ import { SdEvents } from "../../core/events/sd-events";
68
69
  { directive: SdEvents, outputs: ["keydown.capture", "focus.capture", "blur.capture"] },
69
70
  ],
70
71
  host: {
71
- "class": "flex-column fill",
72
+ class: "flex-column fill",
72
73
  "[attr.data-sd-inset]": "inset()",
73
74
  "[attr.data-sd-focus-mode]": "focusMode()",
74
75
  "(keydown.capture)": "onKeydownCapture($event)",
@@ -262,6 +263,7 @@ import { SdEvents } from "../../core/events/sd-events";
262
263
  [canChangeFn]="selection.getCanChangeFn(item)"
263
264
  (valueChange)="selection.toggle(item)"
264
265
  (mousedown)="onSelectorMouseDown($event, rowIdx)"
266
+ (pointerdown)="onSelectorPointerDown($event, rowIdx)"
265
267
  [inline]="true"
266
268
  [theme]="'white'"
267
269
  [disabled]="_selectable !== true"
@@ -370,7 +372,7 @@ import { SdEvents } from "../../core/events/sd-events";
370
372
  border-radius: $border-radius;
371
373
 
372
374
  > ._tool {
373
- background-color: var(--sd-bg-control);
375
+ background-color: var(--sd-bg-canvas);
374
376
  border-top-left-radius: $border-radius;
375
377
  border-top-right-radius: $border-radius;
376
378
  border-bottom: 1px solid $border-color-dark;
@@ -501,7 +503,7 @@ import { SdEvents } from "../../core/events/sd-events";
501
503
 
502
504
  > tbody > tr > td {
503
505
  font-family: var(--sd-font-family-field);
504
- background-color: var(--sd-bg-control);
506
+ background-color: var(--sd-bg-content);
505
507
  vertical-align: top;
506
508
 
507
509
  &._fixed {
@@ -717,6 +719,13 @@ export class SdSheet<TItem> {
717
719
  trackByFn: this.trackByFn,
718
720
  });
719
721
 
722
+ // Drag(paint) selection
723
+ private readonly _dragSelection = injectSheetDragSelection<TItem>({
724
+ domAccessor: this.domAccessor,
725
+ displayItems: this.displayItems,
726
+ selection: this.selection,
727
+ });
728
+
720
729
  // Icons
721
730
  icons = {
722
731
  tablerSettings,
@@ -853,6 +862,10 @@ export class SdSheet<TItem> {
853
862
  });
854
863
  }
855
864
 
865
+ onSelectorPointerDown(event: PointerEvent, r: number): void {
866
+ this._dragSelection.onSelectorPointerDown(event, r);
867
+ }
868
+
856
869
  onSelectorMouseDown(event: MouseEvent, r: number): void {
857
870
  if (!event.shiftKey) return;
858
871
  event.preventDefault();
@@ -276,7 +276,7 @@ const DEFAULT_EXTENSIONS: AnyExtension[] = [
276
276
  display: flex;
277
277
  align-items: center;
278
278
  justify-content: center;
279
- background-color: var(--sd-bg-control);
279
+ background-color: var(--sd-bg-content);
280
280
  font-size: 0.625rem;
281
281
  }
282
282
  }
@@ -12,9 +12,6 @@ export const SD_THEMES = [
12
12
 
13
13
  export type SdThemeName = (typeof SD_THEMES)[number]["value"];
14
14
 
15
- // 밀도 축 — 테마와 직교(DEC-007). compact 는 치수 토큰 그룹(sd-density-compact)만 덮는다.
16
- export type SdDensity = "normal" | "compact";
17
-
18
15
  @Injectable({ providedIn: "root" })
19
16
  export class SdThemeProvider {
20
17
  readonly themes: readonly { value: SdThemeName; label: string }[] = SD_THEMES;
@@ -22,8 +19,6 @@ export class SdThemeProvider {
22
19
  // 라이트(기본) / 블루프린트(도면) / IDE 다크
23
20
  theme = signal<SdThemeName>("light");
24
21
 
25
- density = signal<SdDensity>("normal");
26
-
27
22
  readonly fontSizePresets: readonly number[] = [12, 14, 16, 20, 24, 28];
28
23
 
29
24
  fontSize = signal<number>(12);
@@ -39,10 +34,6 @@ export class SdThemeProvider {
39
34
  }
40
35
  });
41
36
 
42
- effect(() => {
43
- document.body.classList.toggle("sd-density-compact", this.density() === "compact");
44
- });
45
-
46
37
  effect(() => {
47
38
  document.documentElement.style.fontSize = `${this.fontSize()}px`;
48
39
  });
@@ -8,7 +8,6 @@ import {
8
8
  import { SdDropdown } from "../../controls/dropdown/sd-dropdown";
9
9
  import { SdDropdownPopup } from "../../controls/dropdown/sd-dropdown-popup";
10
10
  import { SdButton } from "../../controls/button/sd-button";
11
- import { SdSwitch } from "../../controls/checkbox/sd-switch";
12
11
  import { NgIcon } from "@ng-icons/core";
13
12
  import { SdThemeProvider } from "./sd-theme-provider";
14
13
  import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
@@ -18,7 +17,7 @@ import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
18
17
  changeDetection: ChangeDetectionStrategy.OnPush,
19
18
  encapsulation: ViewEncapsulation.None,
20
19
  standalone: true,
21
- imports: [SdDropdown, SdDropdownPopup, SdButton, SdSwitch, NgIcon],
20
+ imports: [SdDropdown, SdDropdownPopup, SdButton, NgIcon],
22
21
  template: `
23
22
  <sd-dropdown>
24
23
  <sd-button [inline]="true" [theme]="'link-gray'">
@@ -65,14 +64,6 @@ import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
65
64
  }
66
65
  </div>
67
66
  </div>
68
- <div class="flex-row gap-sm cross-align-center">
69
- <span>compact</span>
70
- <sd-switch
71
- [inline]="true"
72
- [value]="_sdTheme.density() === 'compact'"
73
- (valueChange)="_sdTheme.density.set($event ? 'compact' : 'normal')"
74
- />
75
- </div>
76
67
  </div>
77
68
  </sd-dropdown-popup>
78
69
  </sd-dropdown>
@@ -57,9 +57,7 @@ import {
57
57
  ],
58
58
  })
59
59
  export class SdLabel {
60
- theme = input<
61
- "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
62
- >();
60
+ theme = input<"primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray">();
63
61
  color = input<string>();
64
62
  clickable = input(false, { transform: booleanAttribute });
65
63
  }
@@ -57,9 +57,7 @@ import {
57
57
  ],
58
58
  })
59
59
  export class SdNote {
60
- theme = input<
61
- "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
62
- >();
60
+ theme = input<"primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray">();
63
61
  size = input<"sm" | "lg">();
64
62
  inset = input(false, { transform: booleanAttribute });
65
63
  }
@@ -70,7 +70,7 @@ import { PercentPipe } from "@angular/common";
70
70
  &[data-sd-inset="true"] {
71
71
  border-radius: 0;
72
72
  border: none;
73
- background-color: var(--sd-bg-control);
73
+ background-color: var(--sd-bg-track);
74
74
  }
75
75
  }
76
76
  `,
@@ -80,7 +80,7 @@ export class SdProgress {
80
80
  inset = input(false, { transform: booleanAttribute });
81
81
  size = input<"sm" | "lg">();
82
82
  theme = input.required<
83
- "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
83
+ "primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
84
84
  >();
85
85
 
86
86
  value = input.required<number>();
@@ -17,9 +17,7 @@ import { SdSidebarContainer } from "./sd-sidebar-container";
17
17
  host: {
18
18
  "[attr.data-sd-toggle]": "toggle()",
19
19
  },
20
- template: `
21
- <ng-content />
22
- `,
20
+ template: ` <ng-content /> `,
23
21
  styles: [
24
22
  /* language=SCSS */ `
25
23
  @use "../../../scss/commons/mixins";
@@ -32,6 +30,7 @@ import { SdSidebarContainer } from "./sd-sidebar-container";
32
30
  left: 0;
33
31
  width: var(--sd-sidebar-width);
34
32
  height: 100%;
33
+ background-color: var(--sd-bg-canvas);
35
34
  border-right: 1px solid var(--sd-bd-soft);
36
35
 
37
36
  @media not all and (max-width: variables.$breakpoint-mobile) {
@@ -42,6 +42,7 @@ import { tablerMenu2 } from "@ng-icons/tabler-icons";
42
42
  overflow-y: hidden;
43
43
  user-select: none;
44
44
 
45
+ background-color: var(--sd-bg-canvas);
45
46
  color: var(--sd-tx-default);
46
47
  border-bottom: 1px solid var(--sd-bd-soft);
47
48
  padding-left: var(--sd-gap-xs);