@operato/layout 10.2.0 → 10.3.2
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.
- package/CHANGELOG.md +29 -0
- package/dist/src/actions/snackbar.d.ts +69 -2
- package/dist/src/actions/snackbar.js +51 -5
- package/dist/src/actions/snackbar.js.map +1 -1
- package/dist/src/initializer.js +12 -6
- package/dist/src/initializer.js.map +1 -1
- package/dist/src/layouts/ox-header-bar.js +3 -0
- package/dist/src/layouts/ox-header-bar.js.map +1 -1
- package/dist/src/layouts/ox-nav-bar.js +6 -0
- package/dist/src/layouts/ox-nav-bar.js.map +1 -1
- package/dist/src/layouts/ox-snack-bar.d.ts +1 -1
- package/dist/src/layouts/ox-snack-bar.js +251 -49
- package/dist/src/layouts/ox-snack-bar.js.map +1 -1
- package/dist/src/reducers/snackbar.d.ts +6 -0
- package/dist/src/reducers/snackbar.js +8 -0
- package/dist/src/reducers/snackbar.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
var SnackBar_1;
|
|
2
2
|
import { __decorate } from "tslib";
|
|
3
|
-
import '@material/web/button/
|
|
3
|
+
import '@material/web/button/text-button.js';
|
|
4
4
|
import '@material/web/icon/icon.js';
|
|
5
5
|
import { css, html, LitElement, nothing } from 'lit';
|
|
6
6
|
import { customElement, property } from 'lit/decorators.js';
|
|
7
|
+
import { i18next } from '@operato/i18n';
|
|
7
8
|
import { SnackbarController } from '@operato/shell';
|
|
9
|
+
import { resolveNotifyAssistant } from '../actions/snackbar.js';
|
|
8
10
|
let SnackBar = SnackBar_1 = class SnackBar extends LitElement {
|
|
9
11
|
constructor() {
|
|
10
12
|
super(...arguments);
|
|
11
13
|
this._snackbar = new SnackbarController(this);
|
|
12
14
|
this.active = false;
|
|
15
|
+
/* 레벨을 호스트 속성으로 반영해 CSS 가 강조색을 고르게 한다 */
|
|
16
|
+
this.level = 'info';
|
|
13
17
|
// 스와이프 시작 X 좌표 (드래그 중이 아니면 null)
|
|
14
18
|
this.dragStartX = null;
|
|
15
19
|
this.dragX = 0;
|
|
@@ -65,37 +69,100 @@ let SnackBar = SnackBar_1 = class SnackBar extends LitElement {
|
|
|
65
69
|
this.style.setProperty('--snackbar-drag-x', `${x}px`);
|
|
66
70
|
this.style.setProperty('--snackbar-opacity', `${opacity}`);
|
|
67
71
|
}
|
|
72
|
+
/** 기술 원문을 사람이 읽을 수 있는 문자열로. Error 는 message+stack, 그 외는 JSON. */
|
|
73
|
+
formatEx(ex) {
|
|
74
|
+
if (!ex)
|
|
75
|
+
return '';
|
|
76
|
+
if (typeof ex === 'string')
|
|
77
|
+
return ex;
|
|
78
|
+
if (ex instanceof Error)
|
|
79
|
+
return ex.stack || `${ex.name}: ${ex.message}`;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(ex, null, 2);
|
|
82
|
+
}
|
|
83
|
+
catch (_a) {
|
|
84
|
+
return String(ex);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
render() {
|
|
69
|
-
|
|
88
|
+
var _a;
|
|
89
|
+
const { level, message, detail, ex, action } = this._snackbar;
|
|
90
|
+
const tone = level || 'info';
|
|
91
|
+
const exText = this.formatEx(ex);
|
|
92
|
+
/*
|
|
93
|
+
* 주입된 도움 함수. AI 가 없는 앱이거나 설정이 죽어 있으면 null 이고, 그때는 버튼을 그리지
|
|
94
|
+
* 않는다 — 눌러도 아무 일 없는 버튼을 보여주지 않는다.
|
|
95
|
+
*/
|
|
96
|
+
const assistant = resolveNotifyAssistant();
|
|
70
97
|
return html `
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
<md-icon class=${tone} aria-hidden="true">${(_a = SnackBar_1.ICONS[tone]) !== null && _a !== void 0 ? _a : 'info'}</md-icon>
|
|
99
|
+
|
|
100
|
+
<div body>
|
|
101
|
+
<!-- 1층: 무슨 일이 일어났는가 -->
|
|
102
|
+
<div headline>${message}</div>
|
|
103
|
+
|
|
104
|
+
<!-- 2층: 왜·어디서. 낮은 강조로 구분한다 -->
|
|
105
|
+
${detail ? html `<div supporting>${detail}</div>` : nothing}
|
|
106
|
+
|
|
107
|
+
<!-- 3층: 기술 원문. 접힌 채 두고 필요한 사람만 펼친다 -->
|
|
108
|
+
${exText
|
|
109
|
+
? html `
|
|
110
|
+
<details technical>
|
|
111
|
+
<summary>${i18next.t('text.technical detail')}</summary>
|
|
112
|
+
<pre>${exText}</pre>
|
|
113
|
+
</details>
|
|
114
|
+
`
|
|
81
115
|
: nothing}
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
|
|
117
|
+
${(action === null || action === void 0 ? void 0 : action.label) || (assistant && (tone === 'error' || tone === 'warn'))
|
|
84
118
|
? html `
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
<div actions>
|
|
120
|
+
${(action === null || action === void 0 ? void 0 : action.label)
|
|
121
|
+
? html `
|
|
122
|
+
<md-text-button
|
|
123
|
+
@click=${() => {
|
|
124
|
+
var _a;
|
|
125
|
+
this._snackbar.close();
|
|
126
|
+
(_a = action.callback) === null || _a === void 0 ? void 0 : _a.call(action);
|
|
127
|
+
}}
|
|
128
|
+
>${action.label}</md-text-button
|
|
129
|
+
>
|
|
130
|
+
`
|
|
131
|
+
: nothing}
|
|
132
|
+
<!--
|
|
133
|
+
오류·경고에서 곧바로 AI 도움으로 넘어간다. 핸들러가 등록된 앱에서만 나타나므로
|
|
134
|
+
눌러도 아무 일 없는 버튼이 생기지 않는다.
|
|
135
|
+
-->
|
|
136
|
+
${assistant && (tone === 'error' || tone === 'warn')
|
|
137
|
+
? html `
|
|
138
|
+
<md-text-button
|
|
139
|
+
@click=${() => {
|
|
140
|
+
assistant({ level: tone, message, detail, ex });
|
|
141
|
+
this._snackbar.close();
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<md-icon slot="icon">auto_awesome</md-icon>
|
|
145
|
+
${i18next.t('text.ask ai')}
|
|
146
|
+
</md-text-button>
|
|
147
|
+
`
|
|
148
|
+
: nothing}
|
|
149
|
+
</div>
|
|
150
|
+
`
|
|
151
|
+
: nothing}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<md-icon
|
|
155
|
+
close
|
|
156
|
+
role="button"
|
|
157
|
+
tabindex="0"
|
|
158
|
+
title=${i18next.t('text.close')}
|
|
159
|
+
@click=${() => this._snackbar.close()}
|
|
160
|
+
>close</md-icon
|
|
161
|
+
>
|
|
95
162
|
`;
|
|
96
163
|
}
|
|
97
164
|
stateChanged(state) {
|
|
98
|
-
var {
|
|
165
|
+
var { snackbarOpened } = state.snackbar || {};
|
|
99
166
|
// 새로 열릴 때 이전 스와이프 잔상 초기화
|
|
100
167
|
if (snackbarOpened && !this.active) {
|
|
101
168
|
this.dragStartX = null;
|
|
@@ -106,6 +173,7 @@ let SnackBar = SnackBar_1 = class SnackBar extends LitElement {
|
|
|
106
173
|
}
|
|
107
174
|
willUpdate() {
|
|
108
175
|
this.active = this._snackbar.opened;
|
|
176
|
+
this.level = this._snackbar.level || 'info';
|
|
109
177
|
}
|
|
110
178
|
};
|
|
111
179
|
SnackBar.styles = css `
|
|
@@ -120,23 +188,46 @@ SnackBar.styles = css `
|
|
|
120
188
|
*
|
|
121
189
|
* 앱이 필요하면 아래 var 로 조정한다.
|
|
122
190
|
*/
|
|
123
|
-
|
|
191
|
+
/*
|
|
192
|
+
* 글자색은 한 기준(--ox-snack-bar-color)에서 단계를 파생시킨다. 임의의 rgba 를 층마다
|
|
193
|
+
* 따로 적으면 앱이 색을 바꿀 때 한 층만 바뀌어 위계가 깨진다.
|
|
194
|
+
* 1층 기준색 · 2층 72% · 3층 60%
|
|
195
|
+
*/
|
|
196
|
+
--ox-snack-bar-background: rgba(28, 28, 30, 0.96);
|
|
124
197
|
--ox-snack-bar-color: rgba(255, 255, 255, 0.95);
|
|
198
|
+
--ox-snack-bar-supporting-color: color-mix(in srgb, var(--ox-snack-bar-color) 72%, transparent);
|
|
199
|
+
--ox-snack-bar-technical-color: color-mix(in srgb, var(--ox-snack-bar-color) 60%, transparent);
|
|
125
200
|
--ox-snack-bar-radius: var(--md-sys-shape-corner-medium, 8px);
|
|
126
|
-
--ox-snack-bar-padding: 12px;
|
|
127
|
-
|
|
201
|
+
--ox-snack-bar-padding: 12px 14px;
|
|
202
|
+
/*
|
|
203
|
+
* 방향이 있는 그림자여야 떠 있는 느낌이 난다. 이전 값(0 0 10px)은 사방으로 번져
|
|
204
|
+
* 표면이 화면에 붙어 있는 것처럼 보였다. Material elevation 3 에 준한다.
|
|
205
|
+
*/
|
|
206
|
+
--ox-snack-bar-shadow: 0 6px 16px rgba(0, 0, 0, 0.32), 0 2px 6px rgba(0, 0, 0, 0.24);
|
|
207
|
+
/*
|
|
208
|
+
* 레벨 강조색은 **아이콘에만** 쓴다. 배경은 네 레벨 공통(어두운 표면)으로 두었다 —
|
|
209
|
+
* 토스트는 어느 화면 위에도 뜨므로 배경이 레벨마다 흔들리면 안 되고, 띠·패턴 같은 장식을
|
|
210
|
+
* 더하면 짧은 알림에 비해 번잡해진다. 성격은 아이콘 하나로 충분히 전달된다.
|
|
211
|
+
*/
|
|
212
|
+
--ox-snack-bar-accent: var(--status-info-color, #398ace);
|
|
128
213
|
|
|
129
|
-
display:
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: flex-start;
|
|
216
|
+
gap: 10px;
|
|
130
217
|
position: fixed;
|
|
131
218
|
top: 99%;
|
|
132
219
|
left: 0;
|
|
133
220
|
right: 0;
|
|
221
|
+
overflow: hidden;
|
|
134
222
|
border-radius: var(--ox-snack-bar-radius);
|
|
135
223
|
padding: var(--ox-snack-bar-padding);
|
|
136
224
|
color: var(--ox-snack-bar-color);
|
|
137
225
|
background-color: var(--ox-snack-bar-background);
|
|
138
226
|
box-shadow: var(--ox-snack-bar-shadow);
|
|
139
|
-
|
|
227
|
+
/* 왼쪽 정렬 — 이전에는 center 라 두 줄 이상이 되면 줄마다 시작점이 달라 읽기가 나빴다 */
|
|
228
|
+
text-align: left;
|
|
229
|
+
/* 문장 단위로 균형있게 끊는다(지원 브라우저에서만 적용, 그 외 무시) */
|
|
230
|
+
text-wrap: pretty;
|
|
140
231
|
touch-action: pan-y;
|
|
141
232
|
will-change: transform, opacity;
|
|
142
233
|
transform: translate3d(0, 0, 0);
|
|
@@ -145,12 +236,6 @@ SnackBar.styles = css `
|
|
|
145
236
|
visibility: hidden;
|
|
146
237
|
}
|
|
147
238
|
|
|
148
|
-
md-filled-button {
|
|
149
|
-
/* 호스트 테마가 없는 환경(Storybook·단독)에서도 무너지지 않도록 fallback 을 둔다 */
|
|
150
|
-
margin-top: var(--spacing-medium, 8px);
|
|
151
|
-
text-transform: capitalize;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
239
|
:host([active]) {
|
|
155
240
|
visibility: visible;
|
|
156
241
|
transform: translate3d(var(--snackbar-drag-x, 0), -100%, 0);
|
|
@@ -163,33 +248,140 @@ SnackBar.styles = css `
|
|
|
163
248
|
}
|
|
164
249
|
|
|
165
250
|
md-icon {
|
|
166
|
-
|
|
167
|
-
--md-icon-size:
|
|
168
|
-
|
|
251
|
+
flex: none;
|
|
252
|
+
--md-icon-size: 20px;
|
|
253
|
+
/*
|
|
254
|
+
* 아이콘을 headline 첫 줄의 **행 상자 높이**에 맞춘다. 이전에는 margin-top 을 눈대중으로
|
|
255
|
+
* 1px 준 것이라 폰트가 바뀌면 어긋났다. 행간과 같은 높이를 주고 그 안에서 가운데 두면
|
|
256
|
+
* 타입스케일이 달라져도 함께 움직인다.
|
|
257
|
+
*/
|
|
258
|
+
height: var(--md-sys-typescale-title-small-line-height, 1.25rem);
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
color: var(--ox-snack-bar-accent);
|
|
169
262
|
}
|
|
170
263
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
color: var(--ox-snack-bar-
|
|
264
|
+
md-icon[close] {
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
color: var(--ox-snack-bar-supporting-color);
|
|
267
|
+
--md-icon-size: 18px;
|
|
268
|
+
}
|
|
269
|
+
md-icon[close]:hover {
|
|
270
|
+
color: var(--ox-snack-bar-color);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
[body] {
|
|
274
|
+
flex: 1;
|
|
275
|
+
min-width: 0;
|
|
174
276
|
}
|
|
175
277
|
|
|
176
|
-
|
|
177
|
-
|
|
278
|
+
/*
|
|
279
|
+
* 세 층의 크기·굵기는 임의 px 이 아니라 테마의 타입스케일에서 가져온다.
|
|
280
|
+
* 앱 폰트가 바뀌어도 위계가 유지되고, 다른 화면의 본문과 리듬이 맞는다.
|
|
281
|
+
*
|
|
282
|
+
* 1층 title-small (0.875rem / 1.25rem, 500) — 무슨 일이 일어났는가
|
|
283
|
+
* 2층 body-small (0.75rem / 1rem, 400) — 왜·어디서
|
|
284
|
+
* 3층 label-small (0.6875rem) — 기술 원문
|
|
285
|
+
*/
|
|
286
|
+
[headline] {
|
|
287
|
+
font-family: var(--md-sys-typescale-title-small-font, var(--theme-font, inherit));
|
|
288
|
+
font-size: var(--md-sys-typescale-title-small-size, 0.875rem);
|
|
289
|
+
line-height: var(--md-sys-typescale-title-small-line-height, 1.25rem);
|
|
290
|
+
font-weight: var(--md-sys-typescale-title-small-weight, 500);
|
|
291
|
+
/*
|
|
292
|
+
* 단어 중간을 끊지 않는다. anywhere 는 필요 없는 곳에서도 글자를 쪼개 읽기가 나빠진다 —
|
|
293
|
+
* 긴 토큰(URL·ID)만 끊기게 break-word 를 쓴다. 기술 원문에서만 anywhere 를 쓴다.
|
|
294
|
+
*/
|
|
295
|
+
overflow-wrap: break-word;
|
|
178
296
|
}
|
|
179
297
|
|
|
180
|
-
|
|
181
|
-
|
|
298
|
+
[supporting] {
|
|
299
|
+
margin-top: 2px;
|
|
300
|
+
font-family: var(--md-sys-typescale-body-small-font, var(--theme-font, inherit));
|
|
301
|
+
font-size: var(--md-sys-typescale-body-small-size, 0.75rem);
|
|
302
|
+
line-height: var(--md-sys-typescale-body-small-line-height, 1rem);
|
|
303
|
+
font-weight: var(--md-sys-typescale-body-small-weight, 400);
|
|
304
|
+
color: var(--ox-snack-bar-supporting-color);
|
|
305
|
+
overflow-wrap: break-word;
|
|
306
|
+
/* 길어도 토스트가 화면을 삼키지 않게 — 3줄에서 자른다 */
|
|
307
|
+
display: -webkit-box;
|
|
308
|
+
-webkit-box-orient: vertical;
|
|
309
|
+
-webkit-line-clamp: 3;
|
|
310
|
+
overflow: hidden;
|
|
182
311
|
}
|
|
183
312
|
|
|
184
|
-
|
|
185
|
-
|
|
313
|
+
/* 3층 — 기술 원문. 접힌 상태가 기본이고 필요한 사람만 펼친다 */
|
|
314
|
+
[technical] {
|
|
315
|
+
margin-top: 6px;
|
|
316
|
+
font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);
|
|
317
|
+
line-height: var(--md-sys-typescale-label-small-line-height, 1rem);
|
|
318
|
+
}
|
|
319
|
+
[technical] summary {
|
|
320
|
+
cursor: pointer;
|
|
321
|
+
color: var(--ox-snack-bar-supporting-color);
|
|
322
|
+
list-style: none;
|
|
323
|
+
}
|
|
324
|
+
[technical] summary::-webkit-details-marker {
|
|
325
|
+
display: none;
|
|
326
|
+
}
|
|
327
|
+
[technical] summary::before {
|
|
328
|
+
content: '▸';
|
|
329
|
+
display: inline-block;
|
|
330
|
+
margin-right: 4px;
|
|
331
|
+
transition: transform 0.15s;
|
|
332
|
+
}
|
|
333
|
+
[technical][open] summary::before {
|
|
334
|
+
transform: rotate(90deg);
|
|
335
|
+
}
|
|
336
|
+
[technical] summary:hover {
|
|
337
|
+
color: var(--ox-snack-bar-color);
|
|
338
|
+
}
|
|
339
|
+
[technical] pre {
|
|
340
|
+
margin: 6px 0 0;
|
|
341
|
+
padding: 8px;
|
|
342
|
+
max-height: 180px;
|
|
343
|
+
overflow: auto;
|
|
344
|
+
border-radius: 4px;
|
|
345
|
+
background-color: color-mix(in srgb, var(--ox-snack-bar-color) 8%, transparent);
|
|
346
|
+
color: var(--ox-snack-bar-technical-color);
|
|
347
|
+
font-family: 'SF Mono', Consolas, monospace;
|
|
348
|
+
font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);
|
|
349
|
+
line-height: 1.5;
|
|
186
350
|
white-space: pre-wrap;
|
|
351
|
+
/* 원문은 긴 토큰이 흔하므로 여기서는 어디서든 끊는 것이 맞다 */
|
|
352
|
+
overflow-wrap: anywhere;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
[actions] {
|
|
356
|
+
display: flex;
|
|
357
|
+
gap: 4px;
|
|
358
|
+
margin: 6px 0 0 -8px;
|
|
359
|
+
--md-text-button-label-text-color: var(--ox-snack-bar-accent);
|
|
360
|
+
--md-text-button-label-text-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/*
|
|
364
|
+
* 레벨별 강조색 — 프레임워크 상태 색을 그대로 재사용한다.
|
|
365
|
+
* 레벨은 호스트 속성으로 반영한다(:has() 에 의존하지 않는다).
|
|
366
|
+
*/
|
|
367
|
+
:host([level='success']) {
|
|
368
|
+
--ox-snack-bar-accent: var(--ox-snack-bar-success-color, var(--status-success-color, #35a24a));
|
|
369
|
+
}
|
|
370
|
+
:host([level='info']) {
|
|
371
|
+
--ox-snack-bar-accent: var(--ox-snack-bar-info-color, var(--status-info-color, #398ace));
|
|
372
|
+
}
|
|
373
|
+
:host([level='warn']) {
|
|
374
|
+
--ox-snack-bar-accent: var(--ox-snack-bar-warn-color, var(--status-warning-color, #ee8d03));
|
|
375
|
+
}
|
|
376
|
+
:host([level='error']) {
|
|
377
|
+
--ox-snack-bar-accent: var(--ox-snack-bar-error-color, var(--status-danger-color, #d14946));
|
|
187
378
|
}
|
|
188
379
|
|
|
189
380
|
@media (min-width: 460px) {
|
|
190
381
|
:host {
|
|
382
|
+
/* 80% 는 큰 화면에서 한 줄이 지나치게 길어져 읽기 리듬이 깨진다 */
|
|
191
383
|
min-width: 320px;
|
|
192
|
-
max-width:
|
|
384
|
+
max-width: 480px;
|
|
193
385
|
margin: auto;
|
|
194
386
|
width: fit-content;
|
|
195
387
|
}
|
|
@@ -197,9 +389,19 @@ SnackBar.styles = css `
|
|
|
197
389
|
`;
|
|
198
390
|
// 이 거리(px) 이상 스와이프하면 닫힘 처리
|
|
199
391
|
SnackBar.DISMISS_THRESHOLD = 80;
|
|
392
|
+
/** 레벨별 아이콘. 성격을 한눈에 알리는 첫 신호. */
|
|
393
|
+
SnackBar.ICONS = {
|
|
394
|
+
success: 'check_circle',
|
|
395
|
+
info: 'info',
|
|
396
|
+
warn: 'warning',
|
|
397
|
+
error: 'error'
|
|
398
|
+
};
|
|
200
399
|
__decorate([
|
|
201
400
|
property({ type: Boolean, reflect: true })
|
|
202
401
|
], SnackBar.prototype, "active", void 0);
|
|
402
|
+
__decorate([
|
|
403
|
+
property({ type: String, reflect: true })
|
|
404
|
+
], SnackBar.prototype, "level", void 0);
|
|
203
405
|
__decorate([
|
|
204
406
|
property({ type: Object })
|
|
205
407
|
], SnackBar.prototype, "action", void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ox-snack-bar.js","sourceRoot":"","sources":["../../../src/layouts/ox-snack-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/filled-button.js'\nimport '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { SnackbarController } from '@operato/shell'\n\n@customElement('ox-snack-bar')\nclass SnackBar extends LitElement {\n static styles = css`\n :host {\n /*\n * 토스트는 화면 위에 잠깐 떠서 주의를 끄는 요소이므로 **테마와 무관하게 어두운 표면**을 유지한다\n * (Material 스낵바 관례).\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 **다크에서 밝게 반전**되어 눈에 튀었다.\n * → 반투명 어두운 값을 기본값으로 두면 어떤 브랜드 팔레트에서도 안정적이고, .dark 패치도 불필요하다.\n *\n * 앱이 필요하면 아래 var 로 조정한다.\n */\n --ox-snack-bar-background: rgba(0, 0, 0, 0.85);\n --ox-snack-bar-color: rgba(255, 255, 255, 0.95);\n --ox-snack-bar-radius: var(--md-sys-shape-corner-medium, 8px);\n --ox-snack-bar-padding: 12px;\n --ox-snack-bar-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\n\n display: block;\n position: fixed;\n top: 99%;\n left: 0;\n right: 0;\n border-radius: var(--ox-snack-bar-radius);\n padding: var(--ox-snack-bar-padding);\n color: var(--ox-snack-bar-color);\n background-color: var(--ox-snack-bar-background);\n box-shadow: var(--ox-snack-bar-shadow);\n text-align: center;\n touch-action: pan-y;\n will-change: transform, opacity;\n transform: translate3d(0, 0, 0);\n transition-property: visibility, transform, opacity;\n transition-duration: 0.2s;\n visibility: hidden;\n }\n\n md-filled-button {\n /* 호스트 테마가 없는 환경(Storybook·단독)에서도 무너지지 않도록 fallback 을 둔다 */\n margin-top: var(--spacing-medium, 8px);\n text-transform: capitalize;\n }\n\n :host([active]) {\n visibility: visible;\n transform: translate3d(var(--snackbar-drag-x, 0), -100%, 0);\n opacity: var(--snackbar-opacity, 1);\n }\n\n /* 스와이프 중에는 손가락을 즉시 따라오도록 transition 제거 */\n :host([dragging]) {\n transition: none;\n }\n\n md-icon {\n vertical-align: middle;\n --md-icon-size: 1.8em;\n height: fit-content;\n }\n\n /* 상태 아이콘 색 — 어두운 표면 위에 놓이므로 앱이 조정할 수 있게 노출한다(기본값은 기존 값 유지) */\n .info {\n color: var(--ox-snack-bar-info-color, var(--status-info-color));\n }\n\n .warn {\n color: var(--ox-snack-bar-warn-color, var(--status-warning-color));\n }\n\n .error {\n color: var(--ox-snack-bar-error-color, var(--status-danger-color));\n }\n\n pre {\n font-family: inherit;\n white-space: pre-wrap;\n }\n\n @media (min-width: 460px) {\n :host {\n min-width: 320px;\n max-width: 80%;\n margin: auto;\n width: fit-content;\n }\n }\n `\n\n private _snackbar = new SnackbarController(this)\n\n @property({ type: Boolean, reflect: true }) active: boolean = false\n @property({ type: Object }) action: any\n\n // 스와이프 시작 X 좌표 (드래그 중이 아니면 null)\n private dragStartX: number | null = null\n private dragX: number = 0\n // 이 거리(px) 이상 스와이프하면 닫힘 처리\n private static readonly DISMISS_THRESHOLD = 80\n\n connectedCallback() {\n super.connectedCallback()\n // Pointer Events가 터치/마우스를 모두 커버 (스크롤 구분은 touch-action: pan-y가 담당)\n this.addEventListener('pointerdown', this.onDragStart)\n this.addEventListener('pointermove', this.onDragMove)\n this.addEventListener('pointerup', this.onDragEnd)\n }\n\n // CSS 변수로 스와이프 위치/투명도 반영\n private applyDrag(x: number, opacity: number = 1) {\n this.style.setProperty('--snackbar-drag-x', `${x}px`)\n this.style.setProperty('--snackbar-opacity', `${opacity}`)\n }\n\n private onDragStart = (e: PointerEvent) => {\n if (!this.active) return\n this.dragStartX = e.clientX\n this.dragX = 0\n this.setAttribute('dragging', '')\n }\n\n private onDragMove = (e: PointerEvent) => {\n if (this.dragStartX === null) return\n this.dragX = e.clientX - this.dragStartX\n // 스와이프 거리에 따라 점점 투명하게\n const opacity = Math.max(0, 1 - Math.abs(this.dragX) / (SnackBar.DISMISS_THRESHOLD * 2.5))\n this.applyDrag(this.dragX, opacity)\n }\n\n private onDragEnd = () => {\n if (this.dragStartX === null) return\n const moved = Math.abs(this.dragX)\n this.dragStartX = null\n this.removeAttribute('dragging')\n\n if (moved >= SnackBar.DISMISS_THRESHOLD) {\n // 스와이프한 방향으로 밀어내며 닫기\n this.applyDrag(this.dragX > 0 ? window.innerWidth : -window.innerWidth, 0)\n window.setTimeout(() => {\n // transition을 끈 채로 닫아서 아래로 떨어지는 기본 닫기 애니메이션을 숨김\n this.setAttribute('dragging', '')\n this._snackbar.close()\n requestAnimationFrame(() => {\n this.applyDrag(0)\n this.removeAttribute('dragging')\n })\n }, 200)\n } else {\n // 임계값 미달이면 원위치로 복귀\n this.applyDrag(0)\n }\n }\n\n render() {\n const { level, message, action } = this._snackbar\n\n return html`\n ${level\n ? html` <md-icon class=${level}\n >${level == 'info'\n ? html` notification_important `\n : level == 'warn'\n ? html` warning `\n : level == 'error'\n ? html` error `\n : html``}</md-icon\n >`\n : nothing}\n <pre>${message}</pre>\n ${action && action.label\n ? html`\n <md-filled-button\n @click=${() => {\n this._snackbar.close()\n action.callback?.()\n }}\n >${action.label}</md-filled-button\n >\n `\n : html``}\n `\n }\n\n stateChanged(state: any) {\n var { level, message, snackbarOpened, action } = state.snackbar || {}\n\n // 새로 열릴 때 이전 스와이프 잔상 초기화\n if (snackbarOpened && !this.active) {\n this.dragStartX = null\n this.dragX = 0\n this.removeAttribute('dragging')\n this.applyDrag(0)\n }\n }\n\n protected willUpdate() {\n this.active = this._snackbar.opened\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ox-snack-bar.js","sourceRoot":"","sources":["../../../src/layouts/ox-snack-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/text-button.js'\nimport '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { SnackbarController } from '@operato/shell'\n\nimport { resolveNotifyAssistant } from '../actions/snackbar.js'\n\n@customElement('ox-snack-bar')\nclass SnackBar extends LitElement {\n static styles = css`\n :host {\n /*\n * 토스트는 화면 위에 잠깐 떠서 주의를 끄는 요소이므로 **테마와 무관하게 어두운 표면**을 유지한다\n * (Material 스낵바 관례).\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 **다크에서 밝게 반전**되어 눈에 튀었다.\n * → 반투명 어두운 값을 기본값으로 두면 어떤 브랜드 팔레트에서도 안정적이고, .dark 패치도 불필요하다.\n *\n * 앱이 필요하면 아래 var 로 조정한다.\n */\n /*\n * 글자색은 한 기준(--ox-snack-bar-color)에서 단계를 파생시킨다. 임의의 rgba 를 층마다\n * 따로 적으면 앱이 색을 바꿀 때 한 층만 바뀌어 위계가 깨진다.\n * 1층 기준색 · 2층 72% · 3층 60%\n */\n --ox-snack-bar-background: rgba(28, 28, 30, 0.96);\n --ox-snack-bar-color: rgba(255, 255, 255, 0.95);\n --ox-snack-bar-supporting-color: color-mix(in srgb, var(--ox-snack-bar-color) 72%, transparent);\n --ox-snack-bar-technical-color: color-mix(in srgb, var(--ox-snack-bar-color) 60%, transparent);\n --ox-snack-bar-radius: var(--md-sys-shape-corner-medium, 8px);\n --ox-snack-bar-padding: 12px 14px;\n /*\n * 방향이 있는 그림자여야 떠 있는 느낌이 난다. 이전 값(0 0 10px)은 사방으로 번져\n * 표면이 화면에 붙어 있는 것처럼 보였다. Material elevation 3 에 준한다.\n */\n --ox-snack-bar-shadow: 0 6px 16px rgba(0, 0, 0, 0.32), 0 2px 6px rgba(0, 0, 0, 0.24);\n /*\n * 레벨 강조색은 **아이콘에만** 쓴다. 배경은 네 레벨 공통(어두운 표면)으로 두었다 —\n * 토스트는 어느 화면 위에도 뜨므로 배경이 레벨마다 흔들리면 안 되고, 띠·패턴 같은 장식을\n * 더하면 짧은 알림에 비해 번잡해진다. 성격은 아이콘 하나로 충분히 전달된다.\n */\n --ox-snack-bar-accent: var(--status-info-color, #398ace);\n\n display: flex;\n align-items: flex-start;\n gap: 10px;\n position: fixed;\n top: 99%;\n left: 0;\n right: 0;\n overflow: hidden;\n border-radius: var(--ox-snack-bar-radius);\n padding: var(--ox-snack-bar-padding);\n color: var(--ox-snack-bar-color);\n background-color: var(--ox-snack-bar-background);\n box-shadow: var(--ox-snack-bar-shadow);\n /* 왼쪽 정렬 — 이전에는 center 라 두 줄 이상이 되면 줄마다 시작점이 달라 읽기가 나빴다 */\n text-align: left;\n /* 문장 단위로 균형있게 끊는다(지원 브라우저에서만 적용, 그 외 무시) */\n text-wrap: pretty;\n touch-action: pan-y;\n will-change: transform, opacity;\n transform: translate3d(0, 0, 0);\n transition-property: visibility, transform, opacity;\n transition-duration: 0.2s;\n visibility: hidden;\n }\n\n :host([active]) {\n visibility: visible;\n transform: translate3d(var(--snackbar-drag-x, 0), -100%, 0);\n opacity: var(--snackbar-opacity, 1);\n }\n\n /* 스와이프 중에는 손가락을 즉시 따라오도록 transition 제거 */\n :host([dragging]) {\n transition: none;\n }\n\n md-icon {\n flex: none;\n --md-icon-size: 20px;\n /*\n * 아이콘을 headline 첫 줄의 **행 상자 높이**에 맞춘다. 이전에는 margin-top 을 눈대중으로\n * 1px 준 것이라 폰트가 바뀌면 어긋났다. 행간과 같은 높이를 주고 그 안에서 가운데 두면\n * 타입스케일이 달라져도 함께 움직인다.\n */\n height: var(--md-sys-typescale-title-small-line-height, 1.25rem);\n display: flex;\n align-items: center;\n color: var(--ox-snack-bar-accent);\n }\n\n md-icon[close] {\n cursor: pointer;\n color: var(--ox-snack-bar-supporting-color);\n --md-icon-size: 18px;\n }\n md-icon[close]:hover {\n color: var(--ox-snack-bar-color);\n }\n\n [body] {\n flex: 1;\n min-width: 0;\n }\n\n /*\n * 세 층의 크기·굵기는 임의 px 이 아니라 테마의 타입스케일에서 가져온다.\n * 앱 폰트가 바뀌어도 위계가 유지되고, 다른 화면의 본문과 리듬이 맞는다.\n *\n * 1층 title-small (0.875rem / 1.25rem, 500) — 무슨 일이 일어났는가\n * 2층 body-small (0.75rem / 1rem, 400) — 왜·어디서\n * 3층 label-small (0.6875rem) — 기술 원문\n */\n [headline] {\n font-family: var(--md-sys-typescale-title-small-font, var(--theme-font, inherit));\n font-size: var(--md-sys-typescale-title-small-size, 0.875rem);\n line-height: var(--md-sys-typescale-title-small-line-height, 1.25rem);\n font-weight: var(--md-sys-typescale-title-small-weight, 500);\n /*\n * 단어 중간을 끊지 않는다. anywhere 는 필요 없는 곳에서도 글자를 쪼개 읽기가 나빠진다 —\n * 긴 토큰(URL·ID)만 끊기게 break-word 를 쓴다. 기술 원문에서만 anywhere 를 쓴다.\n */\n overflow-wrap: break-word;\n }\n\n [supporting] {\n margin-top: 2px;\n font-family: var(--md-sys-typescale-body-small-font, var(--theme-font, inherit));\n font-size: var(--md-sys-typescale-body-small-size, 0.75rem);\n line-height: var(--md-sys-typescale-body-small-line-height, 1rem);\n font-weight: var(--md-sys-typescale-body-small-weight, 400);\n color: var(--ox-snack-bar-supporting-color);\n overflow-wrap: break-word;\n /* 길어도 토스트가 화면을 삼키지 않게 — 3줄에서 자른다 */\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n }\n\n /* 3층 — 기술 원문. 접힌 상태가 기본이고 필요한 사람만 펼친다 */\n [technical] {\n margin-top: 6px;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: var(--md-sys-typescale-label-small-line-height, 1rem);\n }\n [technical] summary {\n cursor: pointer;\n color: var(--ox-snack-bar-supporting-color);\n list-style: none;\n }\n [technical] summary::-webkit-details-marker {\n display: none;\n }\n [technical] summary::before {\n content: '▸';\n display: inline-block;\n margin-right: 4px;\n transition: transform 0.15s;\n }\n [technical][open] summary::before {\n transform: rotate(90deg);\n }\n [technical] summary:hover {\n color: var(--ox-snack-bar-color);\n }\n [technical] pre {\n margin: 6px 0 0;\n padding: 8px;\n max-height: 180px;\n overflow: auto;\n border-radius: 4px;\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 8%, transparent);\n color: var(--ox-snack-bar-technical-color);\n font-family: 'SF Mono', Consolas, monospace;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1.5;\n white-space: pre-wrap;\n /* 원문은 긴 토큰이 흔하므로 여기서는 어디서든 끊는 것이 맞다 */\n overflow-wrap: anywhere;\n }\n\n [actions] {\n display: flex;\n gap: 4px;\n margin: 6px 0 0 -8px;\n --md-text-button-label-text-color: var(--ox-snack-bar-accent);\n --md-text-button-label-text-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n\n /*\n * 레벨별 강조색 — 프레임워크 상태 색을 그대로 재사용한다.\n * 레벨은 호스트 속성으로 반영한다(:has() 에 의존하지 않는다).\n */\n :host([level='success']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-success-color, var(--status-success-color, #35a24a));\n }\n :host([level='info']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-info-color, var(--status-info-color, #398ace));\n }\n :host([level='warn']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-warn-color, var(--status-warning-color, #ee8d03));\n }\n :host([level='error']) {\n --ox-snack-bar-accent: var(--ox-snack-bar-error-color, var(--status-danger-color, #d14946));\n }\n\n @media (min-width: 460px) {\n :host {\n /* 80% 는 큰 화면에서 한 줄이 지나치게 길어져 읽기 리듬이 깨진다 */\n min-width: 320px;\n max-width: 480px;\n margin: auto;\n width: fit-content;\n }\n }\n `\n\n private _snackbar = new SnackbarController(this)\n\n @property({ type: Boolean, reflect: true }) active: boolean = false\n /* 레벨을 호스트 속성으로 반영해 CSS 가 강조색을 고르게 한다 */\n @property({ type: String, reflect: true }) level: string = 'info'\n @property({ type: Object }) action: any\n\n // 스와이프 시작 X 좌표 (드래그 중이 아니면 null)\n private dragStartX: number | null = null\n private dragX: number = 0\n // 이 거리(px) 이상 스와이프하면 닫힘 처리\n private static readonly DISMISS_THRESHOLD = 80\n\n connectedCallback() {\n super.connectedCallback()\n // Pointer Events가 터치/마우스를 모두 커버 (스크롤 구분은 touch-action: pan-y가 담당)\n this.addEventListener('pointerdown', this.onDragStart)\n this.addEventListener('pointermove', this.onDragMove)\n this.addEventListener('pointerup', this.onDragEnd)\n }\n\n // CSS 변수로 스와이프 위치/투명도 반영\n private applyDrag(x: number, opacity: number = 1) {\n this.style.setProperty('--snackbar-drag-x', `${x}px`)\n this.style.setProperty('--snackbar-opacity', `${opacity}`)\n }\n\n private onDragStart = (e: PointerEvent) => {\n if (!this.active) return\n this.dragStartX = e.clientX\n this.dragX = 0\n this.setAttribute('dragging', '')\n }\n\n private onDragMove = (e: PointerEvent) => {\n if (this.dragStartX === null) return\n this.dragX = e.clientX - this.dragStartX\n // 스와이프 거리에 따라 점점 투명하게\n const opacity = Math.max(0, 1 - Math.abs(this.dragX) / (SnackBar.DISMISS_THRESHOLD * 2.5))\n this.applyDrag(this.dragX, opacity)\n }\n\n private onDragEnd = () => {\n if (this.dragStartX === null) return\n const moved = Math.abs(this.dragX)\n this.dragStartX = null\n this.removeAttribute('dragging')\n\n if (moved >= SnackBar.DISMISS_THRESHOLD) {\n // 스와이프한 방향으로 밀어내며 닫기\n this.applyDrag(this.dragX > 0 ? window.innerWidth : -window.innerWidth, 0)\n window.setTimeout(() => {\n // transition을 끈 채로 닫아서 아래로 떨어지는 기본 닫기 애니메이션을 숨김\n this.setAttribute('dragging', '')\n this._snackbar.close()\n requestAnimationFrame(() => {\n this.applyDrag(0)\n this.removeAttribute('dragging')\n })\n }, 200)\n } else {\n // 임계값 미달이면 원위치로 복귀\n this.applyDrag(0)\n }\n }\n\n /** 레벨별 아이콘. 성격을 한눈에 알리는 첫 신호. */\n private static readonly ICONS: Record<string, string> = {\n success: 'check_circle',\n info: 'info',\n warn: 'warning',\n error: 'error'\n }\n\n /** 기술 원문을 사람이 읽을 수 있는 문자열로. Error 는 message+stack, 그 외는 JSON. */\n private formatEx(ex: any): string {\n if (!ex) return ''\n if (typeof ex === 'string') return ex\n if (ex instanceof Error) return ex.stack || `${ex.name}: ${ex.message}`\n try {\n return JSON.stringify(ex, null, 2)\n } catch {\n return String(ex)\n }\n }\n\n render() {\n const { level, message, detail, ex, action } = this._snackbar\n const tone = level || 'info'\n const exText = this.formatEx(ex)\n /*\n * 주입된 도움 함수. AI 가 없는 앱이거나 설정이 죽어 있으면 null 이고, 그때는 버튼을 그리지\n * 않는다 — 눌러도 아무 일 없는 버튼을 보여주지 않는다.\n */\n const assistant = resolveNotifyAssistant()\n\n return html`\n <md-icon class=${tone} aria-hidden=\"true\">${SnackBar.ICONS[tone] ?? 'info'}</md-icon>\n\n <div body>\n <!-- 1층: 무슨 일이 일어났는가 -->\n <div headline>${message}</div>\n\n <!-- 2층: 왜·어디서. 낮은 강조로 구분한다 -->\n ${detail ? html`<div supporting>${detail}</div>` : nothing}\n\n <!-- 3층: 기술 원문. 접힌 채 두고 필요한 사람만 펼친다 -->\n ${exText\n ? html`\n <details technical>\n <summary>${i18next.t('text.technical detail')}</summary>\n <pre>${exText}</pre>\n </details>\n `\n : nothing}\n\n ${action?.label || (assistant && (tone === 'error' || tone === 'warn'))\n ? html`\n <div actions>\n ${action?.label\n ? html`\n <md-text-button\n @click=${() => {\n this._snackbar.close()\n action.callback?.()\n }}\n >${action.label}</md-text-button\n >\n `\n : nothing}\n <!--\n 오류·경고에서 곧바로 AI 도움으로 넘어간다. 핸들러가 등록된 앱에서만 나타나므로\n 눌러도 아무 일 없는 버튼이 생기지 않는다.\n -->\n ${assistant && (tone === 'error' || tone === 'warn')\n ? html`\n <md-text-button\n @click=${() => {\n assistant({ level: tone as any, message, detail, ex })\n this._snackbar.close()\n }}\n >\n <md-icon slot=\"icon\">auto_awesome</md-icon>\n ${i18next.t('text.ask ai')}\n </md-text-button>\n `\n : nothing}\n </div>\n `\n : nothing}\n </div>\n\n <md-icon\n close\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('text.close')}\n @click=${() => this._snackbar.close()}\n >close</md-icon\n >\n `\n }\n\n stateChanged(state: any) {\n var { snackbarOpened } = state.snackbar || {}\n\n // 새로 열릴 때 이전 스와이프 잔상 초기화\n if (snackbarOpened && !this.active) {\n this.dragStartX = null\n this.dragX = 0\n this.removeAttribute('dragging')\n this.applyDrag(0)\n }\n }\n\n protected willUpdate() {\n this.active = this._snackbar.opened\n this.level = this._snackbar.level || 'info'\n }\n}\n"]}
|
|
@@ -3,16 +3,22 @@ declare const snackbar: (state: {
|
|
|
3
3
|
snackbarOpened: boolean;
|
|
4
4
|
level: string;
|
|
5
5
|
message: string;
|
|
6
|
+
detail: string;
|
|
7
|
+
ex: any;
|
|
6
8
|
action: {};
|
|
7
9
|
} | undefined, action: SnackbarAction) => {
|
|
8
10
|
snackbarOpened: boolean;
|
|
9
11
|
level: string;
|
|
10
12
|
message: string;
|
|
13
|
+
detail: string;
|
|
14
|
+
ex: any;
|
|
11
15
|
action: {};
|
|
12
16
|
} | {
|
|
13
17
|
snackbarOpened: boolean;
|
|
14
18
|
level: import("../index.js").NotificationLevel;
|
|
15
19
|
message: string | undefined;
|
|
20
|
+
detail: string;
|
|
21
|
+
ex: any;
|
|
16
22
|
action: any;
|
|
17
23
|
};
|
|
18
24
|
export default snackbar;
|
|
@@ -3,6 +3,8 @@ const INITIAL_STATE = {
|
|
|
3
3
|
snackbarOpened: false,
|
|
4
4
|
level: '',
|
|
5
5
|
message: '',
|
|
6
|
+
detail: '',
|
|
7
|
+
ex: undefined,
|
|
6
8
|
action: {}
|
|
7
9
|
};
|
|
8
10
|
const snackbar = (state = INITIAL_STATE, action) => {
|
|
@@ -13,6 +15,12 @@ const snackbar = (state = INITIAL_STATE, action) => {
|
|
|
13
15
|
snackbarOpened: true,
|
|
14
16
|
level: action.level || 'info',
|
|
15
17
|
message: action.message,
|
|
18
|
+
/*
|
|
19
|
+
* 새 알림이 이전 알림의 detail·ex 를 물려받으면 안 된다. 값이 없으면 명시적으로 비운다
|
|
20
|
+
* (…state 만 펼치면 직전 오류의 기술 원문이 다음 성공 알림에 붙어 남는다).
|
|
21
|
+
*/
|
|
22
|
+
detail: action.detail || '',
|
|
23
|
+
ex: action.ex,
|
|
16
24
|
action: {
|
|
17
25
|
...action.action
|
|
18
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snackbar.js","sourceRoot":"","sources":["../../../src/reducers/snackbar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAkB,MAAM,wBAAwB,CAAA;AAEtF,MAAM,aAAa,GAAG;IACpB,cAAc,EAAE,KAAK;IACrB,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,EAAE;CACX,CAAA;AAED,MAAM,QAAQ,GAAG,CAAC,KAAK,GAAG,aAAa,EAAE,MAAsB,EAAE,EAAE;IACjE,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,aAAa;YAChB,OAAO;gBACL,GAAG,KAAK;gBACR,cAAc,EAAE,IAAI;gBACpB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,MAAM;gBAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,MAAM,EAAE;oBACN,GAAG,MAAM,CAAC,MAAM;iBACjB;aACF,CAAA;QACH,KAAK,cAAc;YACjB,OAAO;gBACL,GAAG,KAAK;gBACR,cAAc,EAAE,KAAK;aACtB,CAAA;QACH;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC,CAAA;AAED,eAAe,QAAQ,CAAA","sourcesContent":["import { CLOSE_SNACKBAR, OPEN_SNACKBAR, SnackbarAction } from '../actions/snackbar.js'\n\nconst INITIAL_STATE = {\n snackbarOpened: false,\n level: '',\n message: '',\n action: {}\n}\n\nconst snackbar = (state = INITIAL_STATE, action: SnackbarAction) => {\n switch (action.type) {\n case OPEN_SNACKBAR:\n return {\n ...state,\n snackbarOpened: true,\n level: action.level || 'info',\n message: action.message,\n action: {\n ...action.action\n }\n }\n case CLOSE_SNACKBAR:\n return {\n ...state,\n snackbarOpened: false\n }\n default:\n return state\n }\n}\n\nexport default snackbar\n"]}
|
|
1
|
+
{"version":3,"file":"snackbar.js","sourceRoot":"","sources":["../../../src/reducers/snackbar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAkB,MAAM,wBAAwB,CAAA;AAEtF,MAAM,aAAa,GAAG;IACpB,cAAc,EAAE,KAAK;IACrB,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,EAAE;IACV,EAAE,EAAE,SAAgB;IACpB,MAAM,EAAE,EAAE;CACX,CAAA;AAED,MAAM,QAAQ,GAAG,CAAC,KAAK,GAAG,aAAa,EAAE,MAAsB,EAAE,EAAE;IACjE,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,aAAa;YAChB,OAAO;gBACL,GAAG,KAAK;gBACR,cAAc,EAAE,IAAI;gBACpB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,MAAM;gBAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB;;;mBAGG;gBACH,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;gBAC3B,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,MAAM,EAAE;oBACN,GAAG,MAAM,CAAC,MAAM;iBACjB;aACF,CAAA;QACH,KAAK,cAAc;YACjB,OAAO;gBACL,GAAG,KAAK;gBACR,cAAc,EAAE,KAAK;aACtB,CAAA;QACH;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC,CAAA;AAED,eAAe,QAAQ,CAAA","sourcesContent":["import { CLOSE_SNACKBAR, OPEN_SNACKBAR, SnackbarAction } from '../actions/snackbar.js'\n\nconst INITIAL_STATE = {\n snackbarOpened: false,\n level: '',\n message: '',\n detail: '',\n ex: undefined as any,\n action: {}\n}\n\nconst snackbar = (state = INITIAL_STATE, action: SnackbarAction) => {\n switch (action.type) {\n case OPEN_SNACKBAR:\n return {\n ...state,\n snackbarOpened: true,\n level: action.level || 'info',\n message: action.message,\n /*\n * 새 알림이 이전 알림의 detail·ex 를 물려받으면 안 된다. 값이 없으면 명시적으로 비운다\n * (…state 만 펼치면 직전 오류의 기술 원문이 다음 성공 알림에 붙어 남는다).\n */\n detail: action.detail || '',\n ex: action.ex,\n action: {\n ...action.action\n }\n }\n case CLOSE_SNACKBAR:\n return {\n ...state,\n snackbarOpened: false\n }\n default:\n return state\n }\n}\n\nexport default snackbar\n"]}
|