@simplysm/angular 14.0.100 → 14.0.102

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 (97) hide show
  1. package/dist/controls/button/sd-additional-button.js +2 -2
  2. package/dist/controls/button/sd-anchor.js +2 -2
  3. package/dist/controls/button/sd-button.js +2 -2
  4. package/dist/controls/button/sd-modal-select-button.js +2 -2
  5. package/dist/controls/checkbox/sd-checkbox.d.ts.map +1 -1
  6. package/dist/controls/checkbox/sd-checkbox.js +3 -3
  7. package/dist/controls/checkbox/sd-switch.js +2 -2
  8. package/dist/controls/collapse/sd-collapse-icon.d.ts +1 -1
  9. package/dist/controls/collapse/sd-collapse-icon.d.ts.map +1 -1
  10. package/dist/controls/collapse/sd-collapse-icon.js +3 -1
  11. package/dist/controls/collapse/sd-collapse.d.ts +1 -1
  12. package/dist/controls/collapse/sd-collapse.d.ts.map +1 -1
  13. package/dist/controls/collapse/sd-collapse.js +3 -1
  14. package/dist/controls/dropdown/sd-dropdown-popup.js +2 -2
  15. package/dist/controls/gap/sd-gap.js +2 -2
  16. package/dist/controls/input/sd-date-range-picker.js +2 -2
  17. package/dist/controls/input/sd-range.js +2 -2
  18. package/dist/controls/input/sd-textarea.d.ts.map +1 -1
  19. package/dist/controls/input/sd-textarea.js +3 -3
  20. package/dist/controls/input/sd-textfield.d.ts +2 -0
  21. package/dist/controls/input/sd-textfield.d.ts.map +1 -1
  22. package/dist/controls/input/sd-textfield.js +44 -16
  23. package/dist/controls/list/sd-list-item.js +2 -2
  24. package/dist/controls/select/sd-select-button.js +2 -2
  25. package/dist/controls/select/sd-select-item.d.ts.map +1 -1
  26. package/dist/controls/select/sd-select-item.js +3 -3
  27. package/dist/controls/select/sd-select.d.ts.map +1 -1
  28. package/dist/controls/select/sd-select.js +3 -3
  29. package/dist/core/busy/sd-busy-container.js +2 -2
  30. package/dist/core/directive-input-signals.d.ts +0 -9
  31. package/dist/core/directive-input-signals.d.ts.map +1 -1
  32. package/dist/core/modal/sd-modal.js +2 -2
  33. package/dist/core/provideSdAngular.d.ts.map +1 -1
  34. package/dist/core/provideSdAngular.js +12 -0
  35. package/dist/core/ripple/setupRipple.js +1 -1
  36. package/dist/core/toast/sd-toast-container.js +2 -2
  37. package/dist/core/toast/sd-toast.js +2 -2
  38. package/dist/data/crud/sd-base-container.js +4 -4
  39. package/dist/data/crud/sd-crud-list.d.ts +4 -2
  40. package/dist/data/crud/sd-crud-list.d.ts.map +1 -1
  41. package/dist/data/crud/sd-crud-list.js +60 -58
  42. package/dist/data/kanban/sd-kanban-board.js +2 -2
  43. package/dist/data/kanban/sd-kanban-lane.js +2 -2
  44. package/dist/data/kanban/sd-kanban.js +2 -2
  45. package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
  46. package/dist/data/sheet/sd-sheet.js +3 -3
  47. package/dist/features/editor/sd-tiptap-editor.d.ts.map +1 -1
  48. package/dist/features/editor/sd-tiptap-editor.js +3 -3
  49. package/dist/features/theme/sd-theme-provider.d.ts +1 -0
  50. package/dist/features/theme/sd-theme-provider.d.ts.map +1 -1
  51. package/dist/features/theme/sd-theme-provider.js +5 -0
  52. package/dist/features/theme/sd-theme-selector.d.ts.map +1 -1
  53. package/dist/features/theme/sd-theme-selector.js +14 -2
  54. package/dist/features/visual/sd-calendar.js +2 -2
  55. package/dist/features/visual/sd-label.js +2 -2
  56. package/dist/features/visual/sd-note.js +2 -2
  57. package/dist/features/visual/sd-progress.js +2 -2
  58. package/dist/layout/sidebar/sd-sidebar-container.js +2 -2
  59. package/dist/layout/sidebar/sd-sidebar.js +2 -2
  60. package/dist/layout/topbar/sd-topbar.js +2 -2
  61. package/package.json +5 -5
  62. package/scss/commons/_colors.scss +326 -0
  63. package/scss/commons/_mixins.scss +129 -129
  64. package/scss/commons/_theme-variables.scss +8 -1
  65. package/scss/commons/_variables.scss +37 -33
  66. package/scss/controls/_card.scss +1 -1
  67. package/scss/styles.scss +3 -0
  68. package/scss/themes/_blueprint-surfaces.scss +89 -0
  69. package/scss/themes/_variables-blueprint.scss +95 -0
  70. package/scss/themes/_variables-dark.scss +9 -22
  71. package/src/controls/button/sd-button.ts +2 -2
  72. package/src/controls/checkbox/sd-checkbox.ts +13 -11
  73. package/src/controls/collapse/sd-collapse-icon.ts +3 -1
  74. package/src/controls/collapse/sd-collapse.ts +3 -1
  75. package/src/controls/dropdown/sd-dropdown-popup.ts +2 -2
  76. package/src/controls/input/sd-textarea.ts +5 -0
  77. package/src/controls/input/sd-textfield.ts +40 -4
  78. package/src/controls/list/sd-list-item.ts +5 -5
  79. package/src/controls/select/sd-select-button.ts +1 -1
  80. package/src/controls/select/sd-select-item.ts +4 -3
  81. package/src/controls/select/sd-select.ts +2 -1
  82. package/src/core/busy/sd-busy-container.ts +3 -3
  83. package/src/core/directive-input-signals.ts +0 -9
  84. package/src/core/provideSdAngular.ts +14 -0
  85. package/src/core/ripple/setupRipple.ts +1 -1
  86. package/src/data/crud/sd-base-container.ts +3 -3
  87. package/src/data/crud/sd-crud-list.ts +24 -22
  88. package/src/data/kanban/sd-kanban-lane.ts +1 -1
  89. package/src/data/kanban/sd-kanban.ts +1 -1
  90. package/src/data/sheet/sd-sheet.ts +4 -3
  91. package/src/features/editor/sd-tiptap-editor.ts +1 -0
  92. package/src/features/theme/sd-theme-provider.ts +7 -0
  93. package/src/features/theme/sd-theme-selector.ts +4 -0
  94. package/src/layout/sidebar/sd-sidebar-container.ts +3 -3
  95. package/src/layout/sidebar/sd-sidebar.ts +5 -5
  96. package/src/layout/topbar/sd-topbar.ts +1 -1
  97. package/styles.css +487 -103
@@ -62,7 +62,7 @@ import { tablerCheck } from "@ng-icons/tabler-icons";
62
62
 
63
63
  width: calc(var(--font-size-default) + 2px);
64
64
  height: calc(var(--font-size-default) + 2px);
65
- border: 1px solid var(--trans-light);
65
+ border: 1px solid var(--border-color-light);
66
66
  background: var(--theme-secondary-lightest);
67
67
 
68
68
  > ._indicator {
@@ -114,14 +114,9 @@ import { tablerCheck } from "@ng-icons/tabler-icons";
114
114
  }
115
115
  }
116
116
 
117
- &:focus {
118
- > ._indicator_rect {
119
- border-color: var(--theme-#{$key}-default);
120
- }
121
- }
122
-
123
117
  &[data-sd-checked="true"] {
124
118
  > ._indicator_rect {
119
+ border-color: var(--theme-#{$key}-default);
125
120
  background: var(--theme-#{$key}-default);
126
121
 
127
122
  > ._indicator {
@@ -129,6 +124,12 @@ import { tablerCheck } from "@ng-icons/tabler-icons";
129
124
  }
130
125
  }
131
126
  }
127
+
128
+ &:focus {
129
+ > ._indicator_rect {
130
+ border-color: var(--theme-#{$key}-dark);
131
+ }
132
+ }
132
133
  }
133
134
  }
134
135
 
@@ -138,15 +139,16 @@ import { tablerCheck } from "@ng-icons/tabler-icons";
138
139
  border-color: var(--text-trans-lightest);
139
140
  }
140
141
 
141
- &:focus {
142
+ &[data-sd-checked="true"] {
142
143
  > ._indicator_rect {
143
- border-color: var(--text-trans-default);
144
+ border-color: var(--theme-primary-default);
145
+ background: var(--theme-primary-default);
144
146
  }
145
147
  }
146
148
 
147
- &[data-sd-checked="true"] {
149
+ &:focus {
148
150
  > ._indicator_rect {
149
- background: var(--theme-primary-default);
151
+ border-color: var(--theme-primary-dark);
150
152
  }
151
153
  }
152
154
  }
@@ -45,6 +45,8 @@ export class SdCollapseIcon {
45
45
  });
46
46
 
47
47
  transitionStyle = computed(() => {
48
- return this.open() ? "transform 0.1s ease-out" : "transform 0.1s ease-in";
48
+ return this.open()
49
+ ? "transform var(--animation-duration) ease-out"
50
+ : "transform var(--animation-duration) ease-in";
49
51
  });
50
52
  }
@@ -54,7 +54,9 @@ export class SdCollapse {
54
54
 
55
55
  contentTransition = computed(() => {
56
56
  if (!this._initialized()) return "";
57
- return this.open() ? "margin-top 0.1s ease-out" : "margin-top 0.1s ease-in";
57
+ return this.open()
58
+ ? "margin-top var(--animation-duration) ease-out"
59
+ : "margin-top var(--animation-duration) ease-in";
58
60
  });
59
61
 
60
62
  constructor() {
@@ -33,7 +33,7 @@ import { SdResizeDirective } from "../../core/events/sd-resize";
33
33
  z-index: var(--z-index-dropdown);
34
34
  opacity: 0;
35
35
  transform: translateY(-0.625rem);
36
- transition: 0.1s linear;
36
+ transition: var(--animation-duration) linear;
37
37
  transition-property: transform, opacity;
38
38
  pointer-events: none;
39
39
  background: var(--control-color);
@@ -70,7 +70,7 @@ import { SdResizeDirective } from "../../core/events/sd-resize";
70
70
  opacity: 1;
71
71
  pointer-events: auto;
72
72
  transform: none;
73
- animation: sd-dropdown-slide-up 0.2s ease-out;
73
+ animation: sd-dropdown-slide-up var(--animation-duration) ease-out;
74
74
  @include mixins.elevation(8);
75
75
  }
76
76
  }
@@ -56,6 +56,7 @@ import { setupInvalid } from "../../core/validation/setupInvalid";
56
56
  > textarea,
57
57
  > ._contents {
58
58
  @include mixins.form-control-base();
59
+ font-family: var(--font-family-field);
59
60
  resize: none;
60
61
 
61
62
  overflow: auto;
@@ -79,6 +80,10 @@ import { setupInvalid } from "../../core/validation/setupInvalid";
79
80
  }
80
81
  }
81
82
 
83
+ > ._contents > pre {
84
+ font-family: inherit;
85
+ }
86
+
82
87
  &:not([data-sd-inset="true"]):not([data-sd-disabled="true"]) > ._contents {
83
88
  height: 0;
84
89
  max-height: 0;
@@ -1,10 +1,16 @@
1
+ import { isPlatformBrowser } from "@angular/common";
1
2
  import {
2
3
  booleanAttribute,
3
4
  ChangeDetectionStrategy,
4
5
  Component,
5
6
  computed,
7
+ effect,
8
+ ElementRef,
9
+ inject,
6
10
  input,
7
11
  model,
12
+ PLATFORM_ID,
13
+ viewChild,
8
14
  ViewEncapsulation,
9
15
  } from "@angular/core";
10
16
  import { setupInvalid } from "../../core/validation/setupInvalid";
@@ -40,6 +46,7 @@ import {
40
46
  </div>
41
47
  @if (!readonly() && !disabled()) {
42
48
  <input
49
+ #inputEl
43
50
  [style]="inputStyle()"
44
51
  [class]="inputClass()"
45
52
  [attr.title]="title() ?? placeholder()"
@@ -48,11 +55,11 @@ import {
48
55
  [attr.max]="max()"
49
56
  [type]="controlType()"
50
57
  [attr.inputmode]="type() === 'number' ? 'numeric' : undefined"
51
- [value]="controlValue()"
52
58
  [attr.autocomplete]="autocomplete()"
53
59
  [attr.step]="controlStep()"
54
60
  (input)="onInput($event)"
55
61
  (paste)="onInputPaste($event)"
62
+ (blur)="onBlur($event)"
56
63
  />
57
64
  }
58
65
  `,
@@ -70,17 +77,18 @@ import {
70
77
  > input,
71
78
  > ._contents {
72
79
  @include mixins.form-control-base();
80
+ font-family: var(--font-family-field);
73
81
 
74
82
  overflow: auto;
75
83
  width: 100%;
76
84
 
77
- border: 1px solid var(--trans-lighter);
85
+ border: 1px solid var(--border-color-light);
78
86
  border-radius: var(--border-radius-default);
79
87
  background: var(--theme-secondary-lightest);
80
88
 
81
89
  &:focus {
82
90
  outline: none;
83
- border-color: var(--theme-secondary-default);
91
+ border-color: var(--theme-primary-default);
84
92
  }
85
93
 
86
94
  &[type="date"],
@@ -114,6 +122,10 @@ import {
114
122
  display: none;
115
123
  }
116
124
 
125
+ > ._contents > pre {
126
+ font-family: inherit;
127
+ }
128
+
117
129
  &[data-sd-type="number"] {
118
130
  > input,
119
131
  > ._contents {
@@ -253,7 +265,7 @@ import {
253
265
  &[data-sd-disabled="true"] {
254
266
  > ._contents {
255
267
  display: block;
256
- background: var(--theme-gray-lightest);
268
+ background: var(--trans-lightest);
257
269
  color: var(--text-trans-light);
258
270
  }
259
271
 
@@ -316,6 +328,8 @@ export class SdTextfield<K extends keyof SdTextfieldTypes> {
316
328
  "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
317
329
  >();
318
330
 
331
+ private readonly _inputElRef = viewChild<ElementRef<HTMLInputElement>>("inputEl");
332
+
319
333
  private readonly _handler = computed(() => textfieldTypeHandlers[this.type()]);
320
334
 
321
335
  controlType = computed(() => this._handler().controlType);
@@ -340,6 +354,21 @@ export class SdTextfield<K extends keyof SdTextfieldTypes> {
340
354
  });
341
355
 
342
356
  constructor() {
357
+ if (isPlatformBrowser(inject(PLATFORM_ID))) {
358
+ effect(() => {
359
+ const controlValue = this.controlValue();
360
+ const inputEl = this._inputElRef()?.nativeElement;
361
+ if (inputEl == null) return;
362
+
363
+ // 편집(포커스) 중에는 DOM 을 되쓰면 사용자 입력(세그먼트·캐럿·IME 조합)이 깨지므로 생략
364
+ if (document.activeElement === inputEl) return;
365
+
366
+ if (inputEl.value !== controlValue) {
367
+ inputEl.value = controlValue;
368
+ }
369
+ });
370
+ }
371
+
343
372
  setupInvalid(() => {
344
373
  const value = this.value();
345
374
  const handlerErrors = this._handler().validate(value, {
@@ -379,6 +408,13 @@ export class SdTextfield<K extends keyof SdTextfieldTypes> {
379
408
  this.value.set(parsed as SdTextfieldTypes[K]);
380
409
  }
381
410
 
411
+ onBlur(event: FocusEvent): void {
412
+ // 편집 종료 시 모델 기준값으로 동기화 (number 콤마 포맷 반영, date 미완성 입력 정리)
413
+ // date 미완성 입력은 input.value 가 "" 라 비교 없이 무조건 되써야 남은 세그먼트가 정리됨
414
+ const inputEl = event.target as HTMLInputElement;
415
+ inputEl.value = this.controlValue();
416
+ }
417
+
382
418
  onInputPaste(event: ClipboardEvent): void {
383
419
  event.preventDefault();
384
420
  const text = event.clipboardData?.getData("text/plain").trim();
@@ -84,12 +84,12 @@ import { SdRipple } from "../../core/ripple/sd-ripple";
84
84
  &:focus-visible {
85
85
  outline: none;
86
86
  transition: background var(--animation-duration) ease-out;
87
- background: var(--trans-lighter);
87
+ background: var(--trans-light);
88
88
  }
89
89
  }
90
90
 
91
91
  &[data-sd-selected="true"] > ._content {
92
- background: var(--trans-lighter);
92
+ background: var(--trans-light);
93
93
  font-weight: bold;
94
94
  }
95
95
 
@@ -97,7 +97,7 @@ import { SdRipple } from "../../core/ripple/sd-ripple";
97
97
  color: var(--text-trans-default);
98
98
 
99
99
  &:hover {
100
- background: var(--trans-lighter);
100
+ background: var(--trans-light);
101
101
  }
102
102
  }
103
103
 
@@ -128,13 +128,13 @@ import { SdRipple } from "../../core/ripple/sd-ripple";
128
128
  &:not([data-sd-readonly="true"]) {
129
129
  > ._content:hover {
130
130
  transition: background var(--animation-duration) ease-out;
131
- background: var(--trans-lighter);
131
+ background: var(--trans-light);
132
132
  }
133
133
  }
134
134
 
135
135
  // 깊이별 들여쓰기: accordion 항목의 자식 리스트를 한 칸씩 누적 들여씀.
136
136
  // (flat 항목의 자식 리스트는 들여쓰지 않음)
137
- > sd-collapse > ._content > ._children > sd-list {
137
+ > sd-collapse > ._content > ._children > sd-list > sd-list-item > ._content {
138
138
  padding-left: 1.5em;
139
139
  }
140
140
  }
@@ -22,7 +22,7 @@ import { setupRipple } from "../../core/ripple/setupRipple";
22
22
  font-weight: bold;
23
23
  cursor: pointer;
24
24
  color: var(--theme-primary-default);
25
- transition: background 0.1s linear;
25
+ transition: background var(--animation-duration) linear;
26
26
 
27
27
  &:hover {
28
28
  color: var(--theme-primary-darker);
@@ -40,17 +40,18 @@ import { SdGap } from "../gap/sd-gap";
40
40
  display: block;
41
41
  padding: var(--gap-sm) var(--gap-default);
42
42
  cursor: pointer;
43
- transition: background 0.1s ease-in;
43
+ font-family: var(--font-family-field);
44
+ transition: background var(--animation-duration) ease-in;
44
45
  background: var(--control-color);
45
46
 
46
47
  &:hover {
47
- transition: background 0.1s ease-out;
48
+ transition: background var(--animation-duration) ease-out;
48
49
  background: rgba(0, 0, 0, 0.07);
49
50
  }
50
51
 
51
52
  &:focus {
52
53
  outline: none;
53
- transition: background 0.1s ease-out;
54
+ transition: background var(--animation-duration) ease-out;
54
55
  background: rgba(0, 0, 0, 0.07);
55
56
  }
56
57
 
@@ -113,7 +113,7 @@ export type SelectModeValue<T> = {
113
113
  display: flex;
114
114
  overflow: hidden;
115
115
 
116
- border: 1px solid var(--trans-lighter);
116
+ border: 1px solid var(--border-color-light);
117
117
  border-radius: var(--border-radius-default);
118
118
  background: var(--theme-secondary-lightest);
119
119
 
@@ -129,6 +129,7 @@ export type SelectModeValue<T> = {
129
129
  flex-grow: 1;
130
130
  padding: var(--gap-sm) var(--gap-default);
131
131
  cursor: pointer;
132
+ font-family: var(--font-family-field);
132
133
 
133
134
  > ._sd-select-control-content {
134
135
  flex: 1;
@@ -103,7 +103,7 @@ import { SdBusyProvider, type SdBusyType } from "./sd-busy.provider";
103
103
  content: "";
104
104
  height: 4px;
105
105
  width: 100%;
106
- transition: 0.1s ease-in;
106
+ transition: var(--animation-duration) ease-in;
107
107
  transition-property: transform;
108
108
  transform-origin: left;
109
109
  transform: scaleX(0);
@@ -124,7 +124,7 @@ import { SdBusyProvider, type SdBusyType } from "./sd-busy.provider";
124
124
  &[data-sd-type="spinner"] {
125
125
  > ._screen > ._rect {
126
126
  transform: translateY(-100%);
127
- transition: 0.1s ease-in;
127
+ transition: var(--animation-duration) ease-in;
128
128
  transition-property: transform;
129
129
 
130
130
  > ._indicator {
@@ -157,7 +157,7 @@ import { SdBusyProvider, type SdBusyType } from "./sd-busy.provider";
157
157
  &[data-sd-busy="true"] {
158
158
  > ._screen > ._rect {
159
159
  transform: none;
160
- transition: 0.1s ease-out;
160
+ transition: var(--animation-duration) ease-out;
161
161
  }
162
162
  }
163
163
  }
@@ -15,15 +15,6 @@ export type UndefToOptional<T> = {
15
15
  * 컴포넌트/디렉티브의 InputSignal 프로퍼티에서 값 타입을 추출하는 유틸리티 타입
16
16
  *
17
17
  * InputSignal이 아닌 프로퍼티는 제외되며, undefined를 포함하는 필드는 optional로 변환된다.
18
- *
19
- * @example
20
- * ```typescript
21
- * class MyComponent {
22
- * name = input.required<string>();
23
- * age = input(0);
24
- * }
25
- * // DirectiveInputSignals<MyComponent> = { name: string; age: number }
26
- * ```
27
18
  */
28
19
  export type DirectiveInputSignals<T> = UndefToOptional<{
29
20
  [P in keyof T as T[P] extends InputSignal<any> ? P : never]: T[P] extends InputSignal<
@@ -67,6 +67,20 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
67
67
  }
68
68
  });
69
69
 
70
+ const savedBlueprint = sdLocalStorage.get("sd-theme-blueprint");
71
+ if (savedBlueprint != null) {
72
+ sdTheme.blueprint.set(savedBlueprint);
73
+ }
74
+
75
+ let prevBlueprint = sdTheme.blueprint();
76
+ effect(() => {
77
+ const blueprint = sdTheme.blueprint();
78
+ if (blueprint !== prevBlueprint) {
79
+ sdLocalStorage.set("sd-theme-blueprint", blueprint);
80
+ prevBlueprint = blueprint;
81
+ }
82
+ });
83
+
70
84
  const savedFontSize = sdLocalStorage.get("sd-theme-font-size");
71
85
  if (savedFontSize != null) {
72
86
  sdTheme.fontSize.set(savedFontSize);
@@ -42,7 +42,7 @@ export function setupRipple(enableFn?: () => boolean): void {
42
42
  top: y - size + "px",
43
43
  left: x - size + "px",
44
44
 
45
- transition: "var(--animation-duration) linear",
45
+ transition: "calc(2 * var(--animation-duration)) linear",
46
46
  transitionProperty: "transform, opacity",
47
47
  transform: "scale(0.1)",
48
48
  });
@@ -55,9 +55,9 @@ import { NgTemplateOutlet } from "@angular/common";
55
55
  </sd-busy-container>
56
56
 
57
57
  <ng-template #content>
58
- <div class="flex-column fill">
58
+ <div class="flex-column fill bg-control">
59
59
  @if (commandTplRef()) {
60
- <div class="p-sm-default flex-row gap-default bdb bdb-color-lighter bg-theme-gray-lightest">
60
+ <div class="p-sm-default flex-row gap-default bdb bdb-color-lighter">
61
61
  <ng-template [ngTemplateOutlet]="commandTplRef()" />
62
62
  </div>
63
63
  }
@@ -70,7 +70,7 @@ import { NgTemplateOutlet } from "@angular/common";
70
70
 
71
71
  @if (bottomCommandTplRef()) {
72
72
  <div
73
- class="p-sm-default flex-row main-align-end gap-sm bdt bdt-theme-gray-lighter bg-theme-gray-lightest"
73
+ class="p-sm-default flex-row main-align-end gap-sm bdt bdt-theme-gray-lighter"
74
74
  >
75
75
  <ng-template [ngTemplateOutlet]="bottomCommandTplRef()" />
76
76
  </div>
@@ -131,33 +131,33 @@ import {
131
131
  </sd-form>
132
132
  }
133
133
 
134
- @if (!readonly() || toolTplRef()) {
134
+ @if (canCreate() || canDelete() || toolTplRef()) {
135
135
  <div class="flex-row gap-sm p-xs-default">
136
- @if (!readonly()) {
136
+ @if (canCreate()) {
137
137
  <sd-button [size]="'sm'" [theme]="'link-primary'" (click)="create.emit()">
138
138
  <ng-icon [svg]="tablerCirclePlus" />
139
139
  등록
140
140
  </sd-button>
141
- @if (selectMode() !== "single") {
141
+ }
142
+ @if (canDelete() && selectMode() !== "single") {
143
+ <sd-button
144
+ [size]="'sm'"
145
+ [theme]="'link-danger'"
146
+ (click)="delete.emit(currSelectedItems())"
147
+ [disabled]="!hasSelectedNotDeleted()"
148
+ >
149
+ <ng-icon [svg]="tablerEraser" />
150
+ 선택 삭제
151
+ </sd-button>
152
+ @if (hasSelectedDeleted()) {
142
153
  <sd-button
143
154
  [size]="'sm'"
144
- [theme]="'link-danger'"
145
- (click)="delete.emit(currSelectedItems())"
146
- [disabled]="!hasSelectedNotDeleted()"
155
+ [theme]="'link-warning'"
156
+ (click)="restore.emit(currSelectedItems())"
147
157
  >
148
- <ng-icon [svg]="tablerEraser" />
149
- 선택 삭제
158
+ <ng-icon [svg]="tablerRestore" />
159
+ 선택 복구
150
160
  </sd-button>
151
- @if (hasSelectedDeleted()) {
152
- <sd-button
153
- [size]="'sm'"
154
- [theme]="'link-warning'"
155
- (click)="restore.emit(currSelectedItems())"
156
- >
157
- <ng-icon [svg]="tablerRestore" />
158
- 선택 복구
159
- </sd-button>
160
- }
161
161
  }
162
162
  }
163
163
 
@@ -190,7 +190,7 @@ import {
190
190
  [visiblePageCount]="visiblePageCount()"
191
191
  [(sorts)]="sorts"
192
192
  [useAutoSort]="totalPageCount() === 0"
193
- [selectMode]="selectMode() ?? (readonly() ? undefined : 'multi')"
193
+ [selectMode]="selectMode() ?? (canDelete() ? 'multi' : undefined)"
194
194
  [(selectedKeys)]="selectedKeys"
195
195
  [getItemSelectableFn]="getItemSelectableFn()"
196
196
  [trackByFn]="trackByFn()"
@@ -199,7 +199,7 @@ import {
199
199
  [columnControlsInput]="columnControls()"
200
200
  (selectedKeysChange)="onSelectedKeysChange()"
201
201
  >
202
- @if (inlineEditEnabled()) {
202
+ @if (canDelete() && inlineEdit()) {
203
203
  <sd-sheet-column
204
204
  [fixed]="true"
205
205
  [key]="'deleteButton'"
@@ -234,7 +234,9 @@ export class SdCrudList<TItem, TKey> {
234
234
  initialized = input(false);
235
235
  busyCount = model(0);
236
236
  restricted = input(false);
237
- readonly = input(false);
237
+ canCreate = input(true);
238
+ canEdit = input(true);
239
+ canDelete = input(true);
238
240
  inlineEdit = input(true);
239
241
  viewType = input.required<SdViewType>();
240
242
  selectMode = input<"single" | "multi">();
@@ -277,7 +279,7 @@ export class SdCrudList<TItem, TKey> {
277
279
 
278
280
  columnControls = contentChildren(SdSheetColumn);
279
281
 
280
- inlineEditEnabled = computed(() => !this.readonly() && this.inlineEdit());
282
+ inlineEditEnabled = computed(() => this.canEdit() && this.inlineEdit());
281
283
 
282
284
  hasSelectedDeleted = computed(() => this.currSelectedItems().some((it) => this.isDeleted(it)));
283
285
  hasSelectedNotDeleted = computed(() =>
@@ -102,7 +102,7 @@ import { tablerEye, tablerEyeOff } from "@ng-icons/tabler-icons";
102
102
  height: 0;
103
103
  margin-bottom: 0;
104
104
  visibility: hidden;
105
- transition: 0.1s linear;
105
+ transition: var(--animation-duration) linear;
106
106
  transition-property: height, margin-bottom, visibility;
107
107
  }
108
108
  }
@@ -91,7 +91,7 @@ import { SdResizeDirective, type SdResizeEvent } from "../../core/events/sd-resi
91
91
  height: 0;
92
92
  margin-bottom: 0;
93
93
  visibility: hidden;
94
- transition: 0.1s linear;
94
+ transition: var(--animation-duration) linear;
95
95
  transition-property: height, margin-bottom, visibility;
96
96
  }
97
97
 
@@ -361,9 +361,9 @@ import { SdEvents } from "../../core/events/sd-events";
361
361
  $z-index-focus-row-indicator: 6;
362
362
  $z-index-resize-indicator: 7;
363
363
 
364
- $border-color: var(--theme-gray-lighter);
365
- $border-color-dark: var(--theme-gray-lighter);
366
- $border-color-darker: var(--theme-gray-light);
364
+ $border-color: var(--border-color-lighter);
365
+ $border-color-dark: var(--border-color-lighter);
366
+ $border-color-darker: var(--border-color-light);
367
367
 
368
368
  $border-radius: var(--border-radius-default);
369
369
 
@@ -500,6 +500,7 @@ import { SdEvents } from "../../core/events/sd-events";
500
500
  }
501
501
 
502
502
  > tbody > tr > td {
503
+ font-family: var(--font-family-field);
503
504
  background: var(--control-color);
504
505
  vertical-align: top;
505
506
 
@@ -189,6 +189,7 @@ const DEFAULT_EXTENSIONS: AnyExtension[] = [
189
189
  > ._editor-container {
190
190
  padding: var(--gap-sm);
191
191
  min-height: 6.25rem;
192
+ font-family: var(--font-family-field);
192
193
 
193
194
  > .tiptap {
194
195
  outline: none;
@@ -5,6 +5,9 @@ import { isPlatformBrowser } from "@angular/common";
5
5
  export class SdThemeProvider {
6
6
  dark = signal<boolean>(false);
7
7
 
8
+ // 블루프린트(엔지니어링 도면) 테마 — dark 토글과 직교. dark 와 조합해 4면을 만든다.
9
+ blueprint = signal<boolean>(false);
10
+
8
11
  readonly fontSizePresets: readonly number[] = [12, 14, 16, 20, 24, 28];
9
12
 
10
13
  fontSize = signal<number>(12);
@@ -17,6 +20,10 @@ export class SdThemeProvider {
17
20
  document.body.classList.toggle("sd-theme-dark", this.dark());
18
21
  });
19
22
 
23
+ effect(() => {
24
+ document.body.classList.toggle("sd-theme-blueprint", this.blueprint());
25
+ });
26
+
20
27
  effect(() => {
21
28
  document.documentElement.style.fontSize = `${this.fontSize()}px`;
22
29
  });
@@ -54,6 +54,10 @@ import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
54
54
  <span>다크 모드</span>
55
55
  <sd-switch [(value)]="_sdTheme.dark" />
56
56
  </div>
57
+ <div class="flex-row gap-sm cross-align-center">
58
+ <span>블루프린트</span>
59
+ <sd-switch [(value)]="_sdTheme.blueprint" />
60
+ </div>
57
61
  </div>
58
62
  </sd-dropdown-popup>
59
63
  </sd-dropdown>
@@ -25,11 +25,11 @@ import { filter } from "rxjs";
25
25
  position: relative;
26
26
  height: 100%;
27
27
  padding-left: var(--sidebar-width);
28
- transition: padding-left 0.1s ease-out;
28
+ transition: padding-left var(--animation-duration) ease-out;
29
29
 
30
30
  &[data-sd-toggle="true"] {
31
31
  padding-left: 0;
32
- transition: padding-left 0.1s ease-in;
32
+ transition: padding-left var(--animation-duration) ease-in;
33
33
  }
34
34
 
35
35
  > ._backdrop {
@@ -52,7 +52,7 @@ import { filter } from "rxjs";
52
52
  background: var(--background-rev-color);
53
53
  opacity: 0;
54
54
  pointer-events: none;
55
- transition: opacity 0.3s ease-in-out;
55
+ transition: opacity var(--animation-duration) ease-in-out;
56
56
  }
57
57
 
58
58
  &[data-sd-toggle="true"] {
@@ -33,25 +33,25 @@ import { SdSidebarContainer } from "./sd-sidebar-container";
33
33
  width: var(--sidebar-width);
34
34
  height: 100%;
35
35
  //background: var(--control-color);
36
- background: var(--theme-gray-lightest);
36
+ //background: var(--theme-gray-lightest);
37
37
  //border-right: 1px solid var(--border-color-lighter);
38
38
 
39
39
  @media not all and (max-width: variables.$breakpoint-mobile) {
40
- transition: transform 0.1s ease-out;
40
+ transition: transform var(--animation-duration) ease-out;
41
41
 
42
42
  &[data-sd-toggle="true"] {
43
43
  transform: translateX(-100%);
44
- transition: transform 0.1s ease-in;
44
+ transition: transform var(--animation-duration) ease-in;
45
45
  }
46
46
  }
47
47
 
48
48
  @media all and (max-width: variables.$breakpoint-mobile) {
49
- transition: transform 0.3s ease-in;
49
+ transition: transform var(--animation-duration) ease-in;
50
50
  transform: translateX(-100%);
51
51
 
52
52
  &[data-sd-toggle="true"] {
53
53
  transform: none;
54
- transition: transform 0.3s ease-out;
54
+ transition: transform var(--animation-duration) ease-out;
55
55
  @include mixins.elevation(16);
56
56
  }
57
57
  }