@simplysm/angular 14.0.48 → 14.0.51

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 (144) hide show
  1. package/README.md +234 -226
  2. package/dist/controls/select/sd-select.js +3 -3
  3. package/dist/index.d.ts +0 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +0 -2
  6. package/dist/layout/dock/sd-dock-container.js +1 -1
  7. package/dist/styles.css +9 -0
  8. package/docs/bootstrap/provide-sd-angular.md +37 -0
  9. package/docs/bootstrap/sd-angular-config-provider.md +16 -0
  10. package/docs/directives/sd-command-directive.md +30 -0
  11. package/docs/directives/sd-events.md +25 -0
  12. package/docs/directives/sd-intersection-directive.md +36 -0
  13. package/docs/directives/sd-invalid.md +24 -0
  14. package/docs/directives/sd-resize-directive.md +42 -0
  15. package/docs/directives/sd-ripple.md +23 -0
  16. package/docs/directives/sd-router-link.md +38 -0
  17. package/docs/directives/sd-show-effect.md +18 -0
  18. package/docs/directives/sd-typed-template.md +69 -0
  19. package/docs/features/sd-address-search-modal.md +50 -0
  20. package/docs/features/sd-permission-table.md +20 -0
  21. package/docs/features/sd-shared-data-components.md +158 -0
  22. package/docs/features/sd-tiptap-editor.md +26 -0
  23. package/docs/{pipes.md → pipes/format-pipe.md} +14 -5
  24. package/docs/plugins/sd-global-error-handler.md +23 -0
  25. package/docs/{plugins.md → plugins/sd-option-event-plugin.md} +9 -12
  26. package/docs/provider-types/sd-menu.md +65 -0
  27. package/docs/provider-types/sd-modal-content-def.md +148 -0
  28. package/docs/provider-types/sd-toast-content-def.md +73 -0
  29. package/docs/provider-types/shared-data-base.md +59 -0
  30. package/docs/providers/sd-activated-modal-provider.md +34 -0
  31. package/docs/providers/sd-app-structure-provider.md +81 -0
  32. package/docs/providers/sd-busy-provider.md +18 -0
  33. package/docs/providers/sd-file-dialog-provider.md +40 -0
  34. package/docs/providers/sd-local-storage-provider.md +20 -0
  35. package/docs/providers/sd-modal-provider.md +67 -0
  36. package/docs/providers/sd-navigate-window-provider.md +18 -0
  37. package/docs/providers/sd-print-provider.md +25 -0
  38. package/docs/providers/sd-service-client-factory-provider.md +43 -0
  39. package/docs/providers/sd-shared-data-provider.md +64 -0
  40. package/docs/providers/sd-system-config-provider.md +46 -0
  41. package/docs/providers/sd-system-log-provider.md +18 -0
  42. package/docs/providers/sd-theme-provider.md +38 -0
  43. package/docs/providers/sd-toast-provider.md +65 -0
  44. package/docs/recipes/_common-rules.md +244 -0
  45. package/docs/recipes/crud-detail/extension-a-edit-save.md +230 -0
  46. package/docs/recipes/crud-detail/extension-b-delete-restore.md +142 -0
  47. package/docs/recipes/crud-detail/extension-c-modal-view.md +214 -0
  48. package/docs/recipes/crud-detail/extension-d-control-view.md +103 -0
  49. package/docs/recipes/crud-detail/extension-e-auxiliary.md +87 -0
  50. package/docs/recipes/crud-detail/extension-f-complex-detail.md +234 -0
  51. package/docs/recipes/crud-detail.md +215 -722
  52. package/docs/recipes/crud-list/extension-a-inline-edit.md +410 -0
  53. package/docs/recipes/crud-list/extension-b-selection.md +226 -0
  54. package/docs/recipes/crud-list/extension-c-inline-delete.md +87 -0
  55. package/docs/recipes/crud-list/extension-d-select-modal.md +207 -0
  56. package/docs/recipes/crud-list/extension-e-readonly-modal.md +165 -0
  57. package/docs/recipes/crud-list/extension-f-modal-edit.md +177 -0
  58. package/docs/recipes/crud-list/extension-g-excel.md +157 -0
  59. package/docs/recipes/crud-list.md +293 -626
  60. package/docs/recipes/data-select-button.md +194 -101
  61. package/docs/recipes/page-modal-container.md +168 -86
  62. package/docs/styling/classes.md +149 -0
  63. package/docs/styling/mixins.md +100 -0
  64. package/docs/styling/themes.md +35 -0
  65. package/docs/styling/variables.md +147 -0
  66. package/docs/{type-utilities.md → type-utilities/directive-input-signals.md} +17 -35
  67. package/docs/ui-data/sd-list.md +37 -0
  68. package/docs/ui-data/sd-sheet.md +227 -0
  69. package/docs/ui-form/sd-additional-button.md +26 -0
  70. package/docs/ui-form/sd-anchor.md +31 -0
  71. package/docs/ui-form/sd-button.md +105 -0
  72. package/docs/ui-form/sd-checkbox-group.md +39 -0
  73. package/docs/ui-form/sd-checkbox.md +81 -0
  74. package/docs/ui-form/sd-date-range-picker.md +27 -0
  75. package/docs/ui-form/sd-form.md +89 -0
  76. package/docs/ui-form/sd-modal-select-button.md +54 -0
  77. package/docs/ui-form/sd-numpad.md +26 -0
  78. package/docs/ui-form/sd-range.md +26 -0
  79. package/docs/ui-form/sd-select.md +68 -0
  80. package/docs/ui-form/sd-shared-data-select.md +52 -0
  81. package/docs/ui-form/sd-state-preset.md +37 -0
  82. package/docs/ui-form/sd-switch.md +27 -0
  83. package/docs/ui-form/sd-textarea.md +33 -0
  84. package/docs/ui-form/sd-textfield.md +145 -0
  85. package/docs/ui-layout/sd-dock-container.md +64 -0
  86. package/docs/ui-layout/sd-dock.md +37 -0
  87. package/docs/ui-layout/sd-gap.md +26 -0
  88. package/docs/{ui-layout.md → ui-layout/sd-kanban-board.md} +41 -85
  89. package/docs/ui-layout/sd-kanban-lane.md +34 -0
  90. package/docs/ui-layout/sd-kanban.md +29 -0
  91. package/docs/ui-navigation/sd-collapse.md +35 -0
  92. package/docs/ui-navigation/sd-pagination.md +26 -0
  93. package/docs/ui-navigation/sd-sidebar-container.md +49 -0
  94. package/docs/ui-navigation/sd-sidebar-menu.md +22 -0
  95. package/docs/ui-navigation/sd-sidebar-user.md +43 -0
  96. package/docs/ui-navigation/sd-tab.md +51 -0
  97. package/docs/ui-navigation/sd-topbar-container.md +97 -0
  98. package/docs/ui-navigation/sd-topbar-menu.md +23 -0
  99. package/docs/ui-navigation/sd-topbar-user.md +38 -0
  100. package/docs/ui-navigation/sd-topbar.md +30 -0
  101. package/docs/ui-overlay/sd-busy-container.md +69 -0
  102. package/docs/ui-overlay/sd-confirm-modal.md +30 -0
  103. package/docs/ui-overlay/sd-dropdown.md +40 -0
  104. package/docs/ui-overlay/sd-modal.md +34 -0
  105. package/docs/ui-overlay/sd-prompt-modal.md +30 -0
  106. package/docs/ui-overlay/sd-toast.md +35 -0
  107. package/docs/ui-visual/sd-barcode.md +36 -0
  108. package/docs/ui-visual/sd-calendar.md +34 -0
  109. package/docs/ui-visual/sd-echarts.md +32 -0
  110. package/docs/ui-visual/sd-label.md +24 -0
  111. package/docs/ui-visual/sd-note.md +23 -0
  112. package/docs/ui-visual/sd-progress.md +23 -0
  113. package/docs/utils/inject-routing-signals.md +161 -0
  114. package/docs/utils/inject-sd-system-config-resource.md +35 -0
  115. package/docs/utils/mark.md +43 -0
  116. package/docs/utils/selection-managers.md +96 -0
  117. package/docs/utils/set-safe-style.md +19 -0
  118. package/docs/utils/setup-functions.md +93 -0
  119. package/package.json +7 -7
  120. package/scss/commons/_styles.scss +12 -0
  121. package/src/controls/select/sd-select.ts +3 -3
  122. package/src/core/modal/sd-modal.provider.ts +1 -1
  123. package/src/core/modal/sd-modal.ts +1 -1
  124. package/src/core/routing/menu-utils.ts +1 -1
  125. package/src/core/shared-data/sd-shared-data.provider.ts +7 -7
  126. package/src/data/shared-data/sd-shared-data-select.ts +2 -2
  127. package/src/index.ts +0 -3
  128. package/src/layout/dock/sd-dock-container.ts +1 -1
  129. package/dist/data/getOrmDataEditToastErrorMessage.d.ts +0 -2
  130. package/dist/data/getOrmDataEditToastErrorMessage.d.ts.map +0 -1
  131. package/dist/data/getOrmDataEditToastErrorMessage.js +0 -8
  132. package/docs/bootstrap.md +0 -38
  133. package/docs/directives.md +0 -236
  134. package/docs/features.md +0 -169
  135. package/docs/provider-types.md +0 -283
  136. package/docs/providers.md +0 -379
  137. package/docs/styling.md +0 -222
  138. package/docs/ui-data.md +0 -333
  139. package/docs/ui-form.md +0 -502
  140. package/docs/ui-navigation.md +0 -273
  141. package/docs/ui-overlay.md +0 -157
  142. package/docs/ui-visual.md +0 -127
  143. package/docs/utils.md +0 -244
  144. package/src/data/getOrmDataEditToastErrorMessage.ts +0 -10
@@ -1,32 +1,28 @@
1
- # Recipe: 페이지/모달 컨테이너 직접 조립
1
+ # 페이지/모달 컨테이너 직접 조립
2
2
 
3
- 소비 화면이 `<sd-busy-container>` · `<sd-topbar-container>` · `<sd-topbar>` 표준 컴포넌트를 **직접 조립**하여 "하나의 컴포넌트를 페이지·모달·제어(control) 뷰에서 모두 재사용"하는 구조를 만든다. 과거 `<sd-base-container>`가 감추고 있던 분기·타이틀 계산·권한 제한·초기화 숨김을 화면 내부에 인라인으로 풀어쓴다.
3
+ 소비 화면이 `<sd-busy-container>` · `<sd-topbar-container>` · `<sd-topbar>` 표준 컴포넌트를 직접 조립해 하나의 컴포넌트를 page · modal · control 3뷰로 재사용한다. 추상 컨테이너(`<sd-base-container>` 류)가 감추던 분기·타이틀·권한·초기화 숨김을 화면 내부에 인라인으로 풀어 쓴다.
4
4
 
5
- ## 1. Overview
5
+ ## When to use / When NOT to use
6
6
 
7
- - 제거된 추상화: `SdBaseContainer`(`<sd-base-container>`)
8
- - 대체: 소비 컴포넌트가 표준 조각을 `@if` 분기로 직접 조립
9
- - 조립 요소:
10
- - `<sd-busy-container [busy] [message]>`전체 busy 오버레이 (`@simplysm/angular` `SdBusyContainer`, `busy`·`message`·`type` input)
11
- - `<sd-topbar-container>` + `<sd-topbar>` 페이지 뷰에서 상단 헤더
12
- - `@if/@else if` 타입 분기
13
- - `injectViewTypeSignal()` 라우트·모달 컨텍스트에 따라 `"page" | "modal" | "control"` 판정
14
- - `computed()` `header ?? 모달 타이틀 ?? 구조 타이틀` 우선순위 타이틀 계산
15
- - 유지되는 조력자: `SdActivatedModalProvider`, `SdAppStructureProvider`, `SdSystemLogProvider`, `injectCurrentPageCodeSignal`, `injectFullPageCodeSignal`
7
+ - 컴포넌트를 page · modal · control 중 둘 이상으로 재사용한다.
8
+ - page 전용 / modal 전용 / control 전용 화면도 본 레시피에서 필요한 분기만 골라 쓴다 (불필요한 분기는 삭제).
9
+ - `injectViewTypeSignal()`로 자동 판정된 뷰에 따라 topbar·하단 액션 바·본문 레이아웃을 선택한다.
10
+ - ❌ 단일 화면 컨테이너 추상(`<sd-base-container>`, `useBaseContainer()` 등) 재도입 분기·타이틀·권한이 다시 감춰져 화면별 변경이 어려워진다.
11
+ - 리스트 본문 조립이 필요할 [`crud-list.md`](./crud-list.md) (시트·필터·페이징).
12
+ - 상세 폼 본문 조립이 필요할 때 [`crud-detail.md`](./crud-detail.md) (폼·저장 흐름).
13
+ - ❌ modal 뷰의 (a) 다른 화면에서 항목을 골라 돌려주는 **선택 모달** [`crud-list/extension-d-select-modal.md`](./crud-list/extension-d-select-modal.md).
14
+ - ❌ modal 뷰의 (b) 부모 레코드 자식 목록·이력을 input으로 받아 보여주는 **조회 전용 modal** [`crud-list/extension-e-readonly-modal.md`](./crud-list/extension-e-readonly-modal.md).
16
15
 
17
- ## 2. 언제 사용하는가
16
+ ## 전제조건
18
17
 
19
- | 상황 | 적용 여부 |
20
- |---|---|
21
- | 하나의 화면 컴포넌트를 페이지와 모달 양쪽에서 재사용 | 레시피 전체 적용 |
22
- | 페이지 뷰만 필요 (topbar 있는 라우트 화면) | 페이지 블록만 사용, 모달·control 분기 생략 가능 |
23
- | 모달 뷰만 필요 (프로그래밍 방식 `SdModalProvider.showAsync()` 전용) | 모달 블록만 사용 |
24
- | 다른 화면의 영역 일부로 삽입되는 컨트롤 | control 분기(`@else` 블록)만 사용. topbar·모달 분기 생략 |
25
- | 커스텀 단축키·이탈 확인이 필요 | `SdCommandDirective` + `setupCanDeactivate`를 본문에 직접 부착 (본 레시피 범위 외, `features-data-detail.md`류 레시피 참조) |
18
+ - `provideSdAngular({ clientName })` 등록 (앱 부트스트랩) — `SdBusyProvider`·`SdAppStructureProvider` 등 의존 provider가 함께 등록된다.
19
+ - page 뷰 사용 시: 라우트 등록 + `SdAppStructureProvider`에 페이지 코드·타이틀이 정의되어 있어야 `injectViewTitleSignal()`이 페이지 타이틀을 반환한다 (`packages/angular/src/core/routing/injectViewTitleSignal.ts:20`).
20
+ - modal 사용 시: `SdModalProvider.showAsync(...)`로 진입한다. `SdActivatedModalProvider`가 모달 컨텍스트에 자동 주입된다 (`packages/angular/src/core/modal/sd-activated-modal.provider.ts:8`).
21
+ - 공통 규칙: `injectViewTypeSignal()` 호출 시점, page 컴포넌트의 `<sd-topbar>` 소유 [`_common-rules.md`](./_common-rules.md).
26
22
 
27
- ## 3. 완성 예제
23
+ ## 기본 레시피
28
24
 
29
- 아래는 하나의 컴포넌트가 **페이지·모달·control 모두**를 커버하는 완성 형태다. 필요 없는 분기는 삭제하여 단순화할 있다.
25
+ 3뷰를 모두 갖춘 최대 구성이다. 화면 요구에 따라 일부 분기·요소를 생략한다 (생략 기준은 변형](#변형) 참조).
30
26
 
31
27
  ```typescript
32
28
  import { NgIcon } from "@ng-icons/core";
@@ -35,31 +31,26 @@ import {
35
31
  booleanAttribute,
36
32
  ChangeDetectionStrategy,
37
33
  Component,
38
- computed,
39
- inject,
40
34
  input,
41
35
  ViewEncapsulation,
42
36
  } from "@angular/core";
43
37
  import {
44
- injectCurrentPageCodeSignal,
45
- injectFullPageCodeSignal,
38
+ injectViewTitleSignal,
46
39
  injectViewTypeSignal,
47
- SdActivatedModalProvider,
48
- SdAppStructureProvider,
49
40
  SdBusyContainer,
50
- SdSystemLogProvider,
51
41
  SdTopbar,
52
42
  SdTopbarContainer,
53
43
  } from "@simplysm/angular";
54
44
 
55
45
  @Component({
56
- selector: "app-foo",
46
+ selector: "app-foo-view",
57
47
  changeDetection: ChangeDetectionStrategy.OnPush,
58
48
  encapsulation: ViewEncapsulation.None,
59
49
  standalone: true,
60
50
  imports: [SdBusyContainer, SdTopbarContainer, SdTopbar, NgIcon],
61
51
  template: `
62
52
  <sd-busy-container [busy]="busy()" [message]="busyMessage()">
53
+ <!-- initialized() == null(미사용) 또는 true 일 때만 본문 렌더 — 초기 로딩 잔상 방지 -->
63
54
  @if (initialized() == null || initialized()) {
64
55
  @if (restricted()) {
65
56
  <div class="fill tx-theme-gray-light p-xxl tx-center">
@@ -67,27 +58,30 @@ import {
67
58
  <ng-icon [svg]="tablerAlertTriangle" [size]="'5em'" />
68
59
  <br />
69
60
  <br />
70
- '{{ modalOrPageTitle() }}'에 대한 사용권한이 없습니다. 시스템 관리자에게 문의하세요.
61
+ '{{ viewTitle() }}'에 대한 사용권한이 없습니다. 시스템 관리자에게 문의하세요.
71
62
  </div>
72
63
  } @else if (viewType() === "page") {
73
64
  <sd-topbar-container>
74
65
  <sd-topbar>
75
- <h4>{{ modalOrPageTitle() }}</h4>
76
- <!-- 페이지 topbar 보조 영역(버튼·메뉴) 필요 시 여기 -->
66
+ <h4>{{ viewTitle() }}</h4>
67
+ <!-- 페이지 topbar 보조 영역(버튼·메뉴) 필요 시 여기 -->
77
68
  </sd-topbar>
78
69
  <div class="fill">
79
70
  <!-- 본문: 페이지 레이아웃으로 채우기 -->
80
71
  </div>
81
72
  </sd-topbar-container>
82
73
  } @else if (viewType() === "modal") {
74
+ <!--
75
+ modal 용도는 사전에 (a) 선택 모달 / (b) 조회 전용 modal 중 하나로 확정한다.
76
+ (a) → ./crud-list/extension-d-select-modal.md
77
+ (b) → ./crud-list/extension-e-readonly-modal.md
78
+ -->
83
79
  <div class="flex-column fill">
84
80
  <div class="flex-fill">
85
81
  <!-- 본문: 모달 내부 컨텐츠 -->
86
82
  </div>
87
- <!-- 모달 하단 액션(확인/취소 버튼 등) 필요 시:
88
- <div class="bdt bdt-theme-gray-lightest">
89
- ...
90
- </div>
83
+ <!-- 모달 하단 액션(확인/취소) 필요 시:
84
+ <div class="bdt bdt-theme-gray-lightest">...</div>
91
85
  -->
92
86
  </div>
93
87
  } @else {
@@ -97,82 +91,170 @@ import {
97
91
  </sd-busy-container>
98
92
  `,
99
93
  })
100
- export class FooPage {
101
- private readonly _sdActivatedModal = inject(SdActivatedModalProvider, { optional: true });
102
- private readonly _sdAppStructure = inject(SdAppStructureProvider);
103
- private readonly _sdSystemLog = inject(SdSystemLogProvider);
104
-
105
- private readonly _fullPageCode = injectFullPageCodeSignal();
106
- private readonly _currPageCode = injectCurrentPageCodeSignal();
107
-
94
+ export class FooView {
95
+ // injectViewTypeSignal/injectViewTitleSignal은 inject() 내부 호출이므로
96
+ // 필드 이니셜라이저(=생성자 시점)에서만 호출한다 → ./_common-rules.md
108
97
  protected readonly viewType = injectViewTypeSignal();
98
+ protected readonly viewTitle = injectViewTitleSignal();
109
99
 
110
- header = input<string>();
111
100
  initialized = input<boolean | undefined>(undefined);
112
101
  restricted = input(false, { transform: booleanAttribute });
113
102
  busy = input(false, { transform: booleanAttribute });
114
103
  busyMessage = input<string>();
115
104
 
116
- protected readonly modalOrPageTitle = computed(() => {
117
- try {
118
- return (
119
- this.header() ??
120
- this._sdActivatedModal?.modalComponent()?.title() ??
121
- this._sdAppStructure.getTitleByFullCode(this._currPageCode?.() ?? this._fullPageCode())
122
- );
123
- } catch (err) {
124
- void this._sdSystemLog.writeAsync("warn", `modalOrPageTitle 계산 중 에러: ${String(err)}`);
125
- return "";
126
- }
127
- });
128
-
129
105
  protected readonly tablerAlertTriangle = tablerAlertTriangle;
130
106
  }
131
107
  ```
132
108
 
133
- ## 4. 분해 설명
109
+ 타입 자동 판정 규칙 (`packages/angular/src/core/routing/injectViewTypeSignal.ts:7`):
110
+
111
+ 1. `SdActivatedModalProvider`가 주입 가능하면 `"modal"`
112
+ 2. 활성 라우트의 컴포넌트 selector가 호스트 `tagName`과 일치하면 `"page"`
113
+ 3. 그 외는 `"control"`
114
+
115
+ 타이틀 자동 판정 규칙 (`packages/angular/src/core/routing/injectViewTitleSignal.ts:7`):
116
+
117
+ 1. 모달 컨텍스트면 `SdActivatedModalProvider.modalComponent()?.title()`
118
+ 2. 페이지 컨텍스트면 `SdAppStructureProvider.getTitleByFullCode(...)`
119
+ 3. 예외 시 빈 문자열 (별도 try/catch 불필요)
120
+
121
+ ## 변형
122
+
123
+ ### 분기·요소 포함 조건 (생략 가능 기준)
124
+
125
+ 기계적으로 모든 분기·요소를 포함하지 않는다. 화면 요구에 따라 아래 표대로 생략한다.
126
+
127
+ | 요소 | 포함 조건 | 생략하는 경우 예시 |
128
+ |------|----------|-------------------|
129
+ | `viewType() === "page"` 분기 + `<sd-topbar>` | 라우트로 진입하는 페이지 뷰가 필요할 때 | 모달/control 전용 컴포넌트 |
130
+ | `viewType() === "modal"` 분기 | 모달 뷰로도 재사용될 때 | page 전용 또는 control 전용 컴포넌트 |
131
+ | `@else` (control) 분기 | 다른 화면의 영역으로 삽입될 때 | page/modal 전용 컴포넌트 |
132
+ | `busy` / `busyMessage` input | 비동기 작업이 있어서 busy 표시가 필요할 때 | 동기적으로 렌더되는 화면 |
133
+ | `initialized` input | 초기 로딩 완료 전 본문을 숨겨야 할 때 | 초기 로딩 없이 즉시 렌더 가능한 화면 |
134
+ | `restricted` input + 권한 없음 메시지 | 권한 제어가 있는 화면 | 권한 제어가 없는 화면 |
135
+ | `injectViewTitleSignal()` | topbar에 타이틀을 표시할 때 | 타이틀 불필요한 화면 |
136
+
137
+ ### page 전용 (modal·control 분기 생략)
138
+
139
+ ```typescript
140
+ template: `
141
+ <sd-busy-container [busy]="busy()">
142
+ @if (initialized() == null || initialized()) {
143
+ <sd-topbar-container>
144
+ <sd-topbar><h4>{{ viewTitle() }}</h4></sd-topbar>
145
+ <div class="fill"><!-- 본문 --></div>
146
+ </sd-topbar-container>
147
+ }
148
+ </sd-busy-container>
149
+ `,
150
+ ```
151
+
152
+ ### modal 전용 (page·control 분기 생략)
153
+
154
+ ```typescript
155
+ template: `
156
+ <sd-busy-container [busy]="busy()">
157
+ <div class="flex-column fill">
158
+ <div class="flex-fill"><!-- 본문 --></div>
159
+ </div>
160
+ </sd-busy-container>
161
+ `,
162
+ ```
134
163
 
135
- 블록의 역할과 원본 `SdBaseContainer` 코드 대응 지점:
164
+ 분기 안에서 (a) 선택 모달 또는 (b) 조회 전용 modal을 결정한다 — 아래 "🚫 흔한 실수" 섹션의 "modal = 선택 모달 단정 금지" 참조.
136
165
 
137
- | 블록 | 역할 | 원본 대응 |
138
- |---|---|---|
139
- | `<sd-busy-container [busy] [message]>` | 화면 전체에 busy 오버레이를 씌운다. 자식 전체를 감싼다 | `sd-base-container.ts:42` |
140
- | `@if (initialized() == null || initialized())` | `undefined` 또는 `true`일 때만 자식 렌더. `false`면 콘텐츠 전부 숨김(초기화 전 잔상 방지) | `sd-base-container.ts:43` |
141
- | `@if (restricted())` | 권한 없음 시 경고 메시지를 표시하고 콘텐츠는 렌더링하지 않음 | `sd-base-container.ts:44-51` |
142
- | `@else if (viewType() === "page")` | 페이지 뷰: `<sd-topbar-container>` + `<sd-topbar>` 헤더에 계산된 제목 표시 | `sd-base-container.ts:52-63` |
143
- | `@else if (viewType() === "modal")` | 모달 뷰: flex-column 레이아웃, 하단 액션은 선택 | `sd-base-container.ts:64-74` |
144
- | `@else` | control 뷰: 본문만 raw 렌더링 | `sd-base-container.ts:75-77` |
145
- | `modalOrPageTitle` computed | 제목 우선순위 계산 + 예외 시 빈 문자열 + `writeAsync("warn", ...)` | `sd-base-container.ts:102-113` |
166
+ ### control 전용 (page·modal 분기 생략)
146
167
 
147
- ## 5. 뷰 타입 결정
168
+ ```typescript
169
+ template: `
170
+ <sd-busy-container [busy]="busy()">
171
+ <!-- 본문: 다른 화면의 영역으로 삽입 -->
172
+ </sd-busy-container>
173
+ `,
174
+ ```
148
175
 
149
- `injectViewTypeSignal()`은 인자 없이 호출한다. 내부 판정 규칙:
176
+ control 뷰에는 `<sd-topbar-container>`·`<sd-topbar>`를 두지 않는다 (page 컴포넌트가 소유) [`_common-rules.md` "page 컴포넌트가 <sd-topbar-container>와 <sd-topbar>를 소유한다"](./_common-rules.md#page-컴포넌트가-sd-topbar-container와-sd-topbar를-소유한다).
150
177
 
151
- 1. `SdActivatedModalProvider`가 주입 가능하면 **`"modal"`**
152
- 2. 현재 활성 라우트의 컴포넌트 `selector`가 이 화면의 `<host>.tagName`과 일치하면 **`"page"`**
153
- 3. 그 외는 **`"control"`** (다른 화면의 내부에 삽입된 영역)
178
+ ### viewType 수동 오버라이드 (권장하지 않음)
154
179
 
155
- 일반적으로 이 자동 판정으로 충분하다. 수동 오버라이드가 필요한 특수 상황(예: 특정 페이지 안에 자기 자신을 모달처럼 보이게 하고 싶은 경우)에는 아래와 같이 **`injectViewTypeSignal()`은 필드 초기화 시점에 번만 호출**하고 `computed`에서는 signal만 재사용한다.
180
+ 자동 판정으로 충분하지 않은 특수 상황(예: 특정 페이지 안에서 자기 자신을 모달처럼 보이게 )에서만 사용한다. 추상화 복원을 부추기므로 기본은 자동 판정을 쓴다.
156
181
 
157
182
  ```typescript
183
+ import { computed, input } from "@angular/core";
184
+ import { injectViewTypeSignal, type SdViewType } from "@simplysm/angular";
185
+
158
186
  override = input<SdViewType>();
187
+
188
+ // injectViewTypeSignal()은 필드 이니셜라이저에서 한 번만 호출, 이후 computed에서 signal만 읽는다.
159
189
  private readonly _autoViewType = injectViewTypeSignal();
160
190
  protected readonly viewType = computed(() => this.override() ?? this._autoViewType());
161
191
  ```
162
192
 
163
- `injectViewTypeSignal()` 내부는 `inject(SdActivatedModalProvider, { optional: true })` · `inject(ActivatedRoute, { optional: true })`를 호출한다. Angular `inject()`는 injection context(생성자 실행 중 또는 필드 초기화 시점) 안에서만 유효하므로, `computed` 콜백이나 effect 안에서 호출하면 `NG0203` 런타임 에러가 발생한다. 이 오버라이드는 추상화 복원을 부추기므로 **기본은 자동 판정으로 쓰기**를 권장한다.
193
+ ## 🚫 흔한 실수
194
+
195
+ ### `<sd-base-container>` 재도입
196
+
197
+ ```typescript
198
+ // ❌ 단일 컨테이너 추상 컴포넌트로 분기를 다시 감춘다
199
+ template: `
200
+ <sd-base-container [busy]="busy()" [restricted]="restricted()">
201
+ <ng-content />
202
+ </sd-base-container>
203
+ `,
204
+
205
+ // ✅ 표준 조각을 화면이 직접 조립한다 (분기·타이틀·권한 가시화)
206
+ template: `
207
+ <sd-busy-container [busy]="busy()">
208
+ @if (restricted()) { ... }
209
+ @else if (viewType() === "page") { <sd-topbar-container>... </sd-topbar-container> }
210
+ @else if (viewType() === "modal") { ... }
211
+ @else { ... }
212
+ </sd-busy-container>
213
+ `,
214
+ ```
215
+
216
+ **근거**: 추상 컨테이너는 page/modal/control 분기, 타이틀 계산, 권한 차단, 초기화 숨김을 한 번에 감추므로 화면별로 일부 동작만 바꾸기 어렵다. 표준 조각 직접 조립으로 분기를 인라인 노출한다.
217
+
218
+ ### `useBaseContainer()` 같은 공통 헬퍼 추출
164
219
 
165
- ## 6. 타이틀 우선순위
220
+ ```typescript
221
+ // ❌ 공통 헬퍼로 다시 추상화한다 — 결국 <sd-base-container>와 동일한 함정
222
+ const { template } = useBaseContainer({ busy, restricted, initialized });
223
+
224
+ // ✅ 화면 코드에 인라인으로 둔다
225
+ @Component({ template: ` <sd-busy-container [busy]="busy()"> ... </sd-busy-container> ` })
226
+ ```
227
+
228
+ **근거**: 헬퍼 함수 형태로 분기·요소를 묶으면 본 레시피가 제거한 추상화가 다시 생긴다. "한 번만 쓰는 화면별 조립 코드"라는 형태가 의도적이다.
229
+
230
+ ### `viewType() === "modal"`만으로 선택 모달이라고 단정한다
231
+
232
+ ```typescript
233
+ // ❌ modal 뷰면 무조건 SdSelectModal<T> 계약을 부착하고 close.emit으로 결과 반환을 기대한다
234
+ export class FooView implements SdSelectModal<FooItem> {
235
+ selectMode = input<"single" | "multi">();
236
+ selectedItemKeys = input<any[]>();
237
+ close = output<FooItem[] | undefined>();
238
+ // ...
239
+ }
240
+
241
+ // ✅ modal 용도를 사전에 (a)/(b) 중 하나로 확정하고 그 레시피를 따른다
242
+ // (a) 선택 모달: implements SdSelectModal<T> + 하단 액션 바 → ./crud-list/extension-d-select-modal.md
243
+ // (b) 조회 전용 modal: 계약 없음, SdModal 기본 "X"로 닫음 → ./crud-list/extension-e-readonly-modal.md
244
+ ```
166
245
 
167
- 타이틀은 **화면 내부의 `computed`로 직접 계산**한다. 우선순위:
246
+ **근거**: `viewType() === "modal"`은 "모달 컨텍스트에서 렌더 중"만 알려준다. 선택 모달 계약(`SdSelectModal<T>`)은 호출하는 쪽이 `selectMode`를 넘기고 결과를 받는 시나리오 한정이며, 조회 전용 modal과는 input·output·하단 액션 바 구성이 다르다.
168
247
 
169
- 1. `header()` input이 지정되어 있으면 그 값
170
- 2. 모달 컨텍스트면 `SdActivatedModalProvider.modalComponent()?.title()`
171
- 3. 페이지 컨텍스트면 `SdAppStructureProvider.getTitleByFullCode(this._currPageCode?.() ?? this._fullPageCode())`
248
+ ### `injectViewTypeSignal()` 호출 시점 위반 (NG0203)
172
249
 
173
- `getTitleByFullCode`는 구조(`items`) 해당 fullCode 항목이 없으면 `Error`를 던진다. 따라서 `try/catch`로 래핑하고 실패 문자열을 반환하면서 `SdSystemLogProvider.writeAsync("warn", ...)`으로 경고를 남긴다. 이는 화면 생성 시점에 구조 로딩이 지연되는 상황에서 화면 전체가 깨지지 않도록 하기 위함이다.
250
+ `computed`/`effect`/일반 메서드 콜백 안에서 `injectViewTypeSignal()`을 호출하면 injection context를 벗어나 `NG0203` 런타임 에러가 발생한다. 필드 이니셜라이저(=생성자 시점)에서 번만 호출하고 이후엔 반환된 signal만 읽는다 상세·코드 예시는 [`_common-rules.md` "injectViewTypeSignal()은 생성자 또는 필드 이니셜라이저에서만 호출한다"](./_common-rules.md#injectviewtypesignal은-생성자-또는-필드-이니셜라이저에서만-호출한다).
174
251
 
175
- ## 7. 주의사항
252
+ ## 관련 Entry
176
253
 
177
- - **신규 유틸 함수를 추출하지 말 것.** `useBaseContainer()`, `computeModalOrPageTitle()` 같은 공통 헬퍼를 도입하면 레시피가 제거한 추상화가 다시 생긴다. 줄짜리 `computed`를 화면마다 반복하는 편이 낫다.
178
- - **본문 채우기는 화면의 책임이다.** 위 예제의 `<!-- 본문: ... -->` 주석 자리에 `<sd-sheet>`(리스트), `<sd-form>`(상세), 임의 HTML 등 화면별 콘텐츠를 삽입한다. 리스트·상세 화면 조립은 `crud-list.md`·`crud-detail.md` 레시피 참조.
254
+ - [`_common-rules.md`](./_common-rules.md) 차이: 4계열 진입점·확장에 걸친 횡단 규칙 (본 레시피의 `injectViewTypeSignal` 호출 시점·`<sd-topbar>` 소유 규칙 정의 위치).
255
+ - [`crud-list.md`](./crud-list.md) 차이: 리스트 본문(시트·필터·페이징) 조립.
256
+ - [`crud-detail.md`](./crud-detail.md) — 차이: 상세 폼 본문(폼·저장 흐름) 조립.
257
+ - [`crud-list/extension-d-select-modal.md`](./crud-list/extension-d-select-modal.md) — 차이: modal 뷰의 (a) 선택 모달 계약·하단 액션 바.
258
+ - [`crud-list/extension-e-readonly-modal.md`](./crud-list/extension-e-readonly-modal.md) — 차이: modal 뷰의 (b) 조회 전용 패턴.
259
+ - [`crud-detail/extension-c-modal-view.md`](./crud-detail/extension-c-modal-view.md) — 차이: 상세 폼의 modal 분기(canDeactivate·하단 액션 템플릿).
260
+ - [`crud-detail/extension-d-control-view.md`](./crud-detail/extension-d-control-view.md) — 차이: 상세 폼의 control 분기(마스터-디테일 디테일 영역).
@@ -0,0 +1,149 @@
1
+ # CSS Classes
2
+
3
+ 전역 유틸리티 CSS 클래스. `@layer base, theme-variant, utilities` 순서로 구성되며, 유틸리티 클래스는 `@layer utilities`에 정의된다.
4
+
5
+ ## Layout
6
+
7
+ ### Flex
8
+
9
+ | Class | Description |
10
+ |-------|-------------|
11
+ | `.flex-row` | `display: flex; flex-direction: row` (기본 gap 없음) |
12
+ | `.flex-column` | `display: flex; flex-direction: column` (기본 gap 없음) |
13
+ | `.flex-row-inline` | `display: inline-flex; flex-direction: row` |
14
+ | `.flex-column-inline` | `display: inline-flex; flex-direction: column` |
15
+ | `.flex-fill` | `flex: 1 1 auto; overflow: auto` |
16
+ | `.flex-auto` | `flex: 1 0 auto` |
17
+ | `.flex-min` | `flex: 0 0 0` |
18
+
19
+ ### Grid
20
+
21
+ | Class | Description |
22
+ |-------|-------------|
23
+ | `.grid` | `display: grid; grid-template-columns: repeat(12, 1fr)` |
24
+ | `.grid-{1..12}` | `grid-column-end: span {n}` |
25
+ | `.grid-sm-{1..12}` | `@media (max-width: 1280px)` 반응형 |
26
+ | `.grid-xs-{1..12}` | `@media (max-width: 1024px)` 반응형 |
27
+ | `.grid-xxs-{1..12}` | `@media (max-width: 800px)` 반응형 |
28
+
29
+ ### Alignment
30
+
31
+ | Class | Description |
32
+ |-------|-------------|
33
+ | `.main-align-start` | `justify-content: start` |
34
+ | `.main-align-end` | `justify-content: end` |
35
+ | `.main-align-center` | `justify-content: center` |
36
+ | `.cross-align-start` | `align-items: start` |
37
+ | `.cross-align-end` | `align-items: end` |
38
+ | `.cross-align-center` | `align-items: center` |
39
+
40
+ ## Spacing
41
+
42
+ gap 키: `xxs`, `xs`, `sm`, `default`, `lg`, `xl`, `xxl`, `0`, `auto`
43
+
44
+ | Pattern | Description |
45
+ |---------|-------------|
46
+ | `.p-{key}` | 전체 padding |
47
+ | `.pv-{key}` | 수직 padding (top + bottom) |
48
+ | `.ph-{key}` | 수평 padding (left + right) |
49
+ | `.pt-{key}`, `.pr-{key}`, `.pb-{key}`, `.pl-{key}` | 방향별 padding |
50
+ | `.m-{key}` | 전체 margin |
51
+ | `.mv-{key}` | 수직 margin |
52
+ | `.mh-{key}` | 수평 margin |
53
+ | `.mt-{key}`, `.mr-{key}`, `.mb-{key}`, `.ml-{key}` | 방향별 margin |
54
+ | `.gap-{key}` | flex/grid gap |
55
+ | `.sw-{key}` | width |
56
+ | `.sh-{key}` | height |
57
+ | `.p-{key}-{key}` | padding: vertical horizontal |
58
+ | `.m-{key}-{key}` | margin: vertical horizontal |
59
+ | `.t-{key}`, `.r-{key}`, `.b-{key}`, `.l-{key}` | position offset (top, right, bottom, left) |
60
+
61
+ ## Card
62
+
63
+ | Class | Description |
64
+ |-------|-------------|
65
+ | `.card` | 카드 컨테이너 (배경, 그림자, 진입 애니메이션) |
66
+
67
+ ## Form
68
+
69
+ | Class | Description |
70
+ |-------|-------------|
71
+ | `.form-box` | 수직 폼 레이아웃 (`flex-direction: column`, gap `default`) |
72
+ | `.form-box-inline` | 인라인 폼 레이아웃 (`inline-flex`, `flex-wrap: wrap`, gap `sm`) |
73
+ | `.form-table` | 테이블 형태 폼 레이아웃 (`display: table`) |
74
+ | `.form-table-header` | form-table 내 섹션 헤더 (`<th class="form-table-header">`) |
75
+ | `.form-control` | 기본 폼 컨트롤 스타일 (padding, font, line-height) |
76
+
77
+ ## Table
78
+
79
+ | Class | Description |
80
+ |-------|-------------|
81
+ | `.table` | 기본 테이블 스타일 (border, spacing) |
82
+ | `.table-inset` | 외곽 border 제거 |
83
+ | `.table-inline` | `width: auto` |
84
+ | `.table-bd-v` | 수직 border만 제거 |
85
+ | `.table-bd-h` | 수평 border만 제거 |
86
+
87
+ ## Display
88
+
89
+ | Class | Description |
90
+ |-------|-------------|
91
+ | `.block` | `display: block` |
92
+ | `.inline-block` | `display: inline-block` |
93
+ | `.inline` | `display: inline` |
94
+ | `.fill` | `height: 100%; width: 100%; overflow: auto` |
95
+ | `.nowrap` | `white-space: nowrap` |
96
+ | `.overflow-auto` | `overflow: auto` |
97
+ | `.position-relative` | `position: relative` |
98
+ | `.sticky-top` | `position: sticky; top: 0; z-index: 1` |
99
+
100
+ ## Text
101
+
102
+ | Pattern | Description |
103
+ |---------|-------------|
104
+ | `.ft-size-{key}` | 폰트 크기 (key: h1, h2, h3, h4, h5, h6, lg, default, sm) |
105
+ | `.tx-left`, `.tx-right`, `.tx-center` | text-align |
106
+ | `.tx-line-through` | 취소선 |
107
+ | `.tx-underline` | 밑줄 |
108
+ | `.tx-trans-{key}` | 텍스트 투명도 색상 (key: dark, default, light, lighter, lightest, rev-default, rev-dark, rev-darker) |
109
+ | `.tx-theme-{theme}-{shade}` | 테마별 텍스트 색상 |
110
+
111
+ ## Background
112
+
113
+ | Pattern | Description |
114
+ |---------|-------------|
115
+ | `.bg-theme-{theme}-{shade}` | 테마별 배경색 |
116
+ | `.bg-trans-{key}` | 투명도 배경색 |
117
+ | `.bg-default` | 기본 배경색 (`--background-color`) |
118
+ | `.bg-control` | 컨트롤 배경색 (`--control-color`) |
119
+
120
+ ## Border
121
+
122
+ | Pattern | Description |
123
+ |---------|-------------|
124
+ | `.bd` | `border: 1px solid` |
125
+ | `.bd-none` | `border: none` |
126
+ | `.bd-transparent` | `border-color: transparent` |
127
+ | `.bd-theme-{theme}-{shade}` | 테마별 border 색상 |
128
+ | `.bd-trans-{key}` | 투명도 border 색상 |
129
+ | `.bd-color-{key}` | border-color 변수 (key: lighter, light, default, dark, darker) |
130
+ | `.bd{d}` | 방향별 border (d: t, r, b, l) |
131
+ | `.bd{d}-theme-{theme}-{shade}` | 방향별 테마 border 색상 |
132
+ | `.bd{d}-trans-{key}` | 방향별 투명도 border 색상 |
133
+ | `.bd{d}-color-{key}` | 방향별 border-color 변수 |
134
+ | `.bd{d}-none` | 방향별 border 제거 |
135
+ | `.bd{d}-transparent` | 방향별 border 투명 |
136
+ | `.bd-width-{key}` | border 두께 |
137
+ | `.bd{d}-width-{key}` | 방향별 border 두께 |
138
+ | `.bd-radius-{key}` | border-radius (key: xs, sm, default, lg, xl, xxl) |
139
+ | `.bd{d}-radius-{key}` | 방향별 border-radius (d: t, b, l, r) |
140
+
141
+ ## Special
142
+
143
+ | Class | Description |
144
+ |-------|-------------|
145
+ | `.help` | 도움말 스타일 (dotted underline, help cursor) |
146
+ | `.control-header` | 컨트롤 헤더 (작은 폰트, 회색) |
147
+ | `.page-header` | 페이지 헤더 (작은 폰트, 회색, 하단 여백) |
148
+ | `.sh-topbar` | `height: var(--topbar-height)` |
149
+ | `.sw-sidebar` | `width: var(--sidebar-width)` |
@@ -0,0 +1,100 @@
1
+ # Mixins / Functions
2
+
3
+ 공개 SCSS mixin과 function. `scss/commons/_mixins.scss`에 정의된다.
4
+
5
+ ## Mixins
6
+
7
+ ### `writeVars($value, $prevKey)`
8
+
9
+ SCSS 맵을 CSS 커스텀 프로퍼티로 변환하여 출력한다. 중첩 맵은 키를 `-`로 연결한다.
10
+
11
+ ```scss
12
+ @include mixins.writeVars(variables.$vars, "");
13
+ // 출력: --theme-gray-lightest: ...; --theme-gray-lighter: ...; ...
14
+ ```
15
+
16
+ | Param | Type | Description |
17
+ |-------|------|-------------|
18
+ | `$value` | `map \| value` | 변환할 SCSS 맵 또는 값 |
19
+ | `$prevKey` | `string` | 키 접두사 (루트 호출 시 `""`) |
20
+
21
+ ### `elevation($value)`
22
+
23
+ Material Design 스타일 box-shadow를 적용한다.
24
+
25
+ ```scss
26
+ @include mixins.elevation(4); // 양수: 외부 그림자
27
+ @include mixins.elevation(-2); // 음수: 내부(inset) 그림자
28
+ @include mixins.elevation(0); // 그림자 제거
29
+ ```
30
+
31
+ | Param | Type | Description |
32
+ |-------|------|-------------|
33
+ | `$value` | `number` | 그림자 강도. 양수: 외부, 음수: 내부, 0/none: 제거 |
34
+
35
+ ### `form-control-base()`
36
+
37
+ ��본 폼 컨트롤 스타일. padding, font, line-height, color를 설정한다. 매개변수 없음.
38
+
39
+ ```scss
40
+ @include mixins.form-control-base();
41
+ // display: block; padding: var(--gap-sm) var(--gap-default);
42
+ // font-size, font-family, font-variant-numeric, line-height, color
43
+ ```
44
+
45
+ ### `help()`
46
+
47
+ 도움말 스타일 (점선 밑줄 + help 커서). 매개변수 없음.
48
+
49
+ ```scss
50
+ @include mixins.help();
51
+ // text-decoration-line: underline; text-decoration-style: dotted; cursor: help;
52
+ ```
53
+
54
+ ### `border-direction-variants($dir, $d)`
55
+
56
+ 특정 방향의 border 유틸리티 클래스를 일괄 생성한다. 내부적으로 `_styles.scss`에서 4방향에 대해 호출된다.
57
+
58
+ | Param | Type | Description |
59
+ |-------|------|-------------|
60
+ | `$dir` | `string` | CSS 방향 (`top`, `right`, `bottom`, `left`) |
61
+ | `$d` | `string` | 약어 (`t`, `r`, `b`, `l`) |
62
+
63
+ ### `flex-direction($direction, $defaultGap?)`
64
+
65
+ flex-direction과 gap을 설정한다. Chrome 84 이하(flex gap 미지원)를 위한 margin 폴백을 포함한다.
66
+
67
+ ```scss
68
+ @include mixins.flex-direction(row, var(--gap-sm));
69
+ // flex-direction: row; gap: var(--gap-sm);
70
+ // + @supports not (appearance: auto) { gap: 0; > * + * { margin-left: ... } }
71
+ ```
72
+
73
+ | Param | Type | Description |
74
+ |-------|------|-------------|
75
+ | `$direction` | `row \| column` | flex 방향 |
76
+ | `$defaultGap` | `value \| null` | 기본 gap 값 (선택) |
77
+
78
+ ## Functions
79
+
80
+ ### `to-rgb($oklch-color)`
81
+
82
+ OKLCH 색상을 RGB 색상 공간으로 변환한다. `_variables.scss`에 정의된다.
83
+
84
+ ```scss
85
+ $blue: to-rgb(oklch(0.707 0.165 254.624));
86
+ ```
87
+
88
+ ### `color-map($base, $offset?)`
89
+
90
+ 기본 색상에서 7단계(lightest~darkest) shade 맵을 생성한다. `_variables.scss`에 정의된다.
91
+
92
+ ```scss
93
+ $primary: color-map(map.get($colors, blue));
94
+ // (lightest: ..., lighter: ..., light: ..., default: ..., dark: ..., darker: ..., darkest: ...)
95
+ ```
96
+
97
+ | Param | Type | Description |
98
+ |-------|------|-------------|
99
+ | `$base` | `color` | 기본 색상 |
100
+ | `$offset` | `percentage` | lightness 오프셋 (기본 `0%`) |
@@ -0,0 +1,35 @@
1
+ # Themes
2
+
3
+ ## `.sd-theme-dark`
4
+
5
+ 다크 모드 테마 클래스. `<body>`에 `SdThemeProvider`가 자동으로 토글한다.
6
+
7
+ `@layer theme-variant`에 정의되며, `:root`의 CSS 커스텀 프로퍼티를 오버라이드한다.
8
+
9
+ ### 오버라이드 변수
10
+
11
+ | Category | Description |
12
+ |----------|-------------|
13
+ | `--theme-{group}-{shade}` | 모든 테마 색상이 반전된다. lightest가 어두운 톤, darkest가 밝은 톤으로 변환 |
14
+ | `--trans-*` | 투명도 값이 흰색 기반(`rgba(255,255,255,...)`)으로 변경 |
15
+ | `--text-trans-*` | 텍스트 색상이 흰색 기반으로 변경 (`dark: white`, `default: rgba(255,255,255,0.87)` 등) |
16
+ | `--border-color-*` | border 색상이 다크 테마 기준으로 변경 |
17
+ | `--background-color` | `#000` |
18
+ | `--background-rev-color` | `#fff` |
19
+ | `--control-color` | `#000` |
20
+ | `--busy-overlay-bg` | `rgba(0, 0, 0, 0.6)` |
21
+ | `--sheet-bg` | 다크 테마 시트 배경 |
22
+
23
+ ### 이미지 반전
24
+
25
+ ```css
26
+ .sd-theme-dark img:not(.no-invert) {
27
+ filter: invert(1) hue-rotate(180deg);
28
+ }
29
+ ```
30
+
31
+ 다크 모드에서 모든 `<img>`가 자동 반전된다. 반전을 원하지 않는 이미지에 `class="no-invert"`를 추가한다.
32
+
33
+ ### 활성화
34
+
35
+ `SdThemeProvider.dark` signal이 `true`일 때 `<body>`에 `.sd-theme-dark` 클래스가 추가된다. `provideSdAngular`가 localStorage와 자동 동기화한다.