@operato/layout 10.29.3 → 10.29.7

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.
@@ -71,6 +71,41 @@ export declare function registerNotifyAssistant(assistant: NotifyAssistant | nul
71
71
  * 토스트는 이 결과가 null 이 아닐 때만 버튼을 그린다.
72
72
  */
73
73
  export declare function resolveNotifyAssistant(): ((request: NotifyAssistantRequest) => void) | null;
74
+ /**
75
+ * 알림에 단추를 하나 끼워 넣는 연결점 — 알림의 **뜻을 아는 쪽**이 주입한다.
76
+ *
77
+ * 토스트는 오류가 무엇을 뜻하는지 모른다. 권한 거절에서 「권한 요청」을 띄우는 일은 권한을 아는
78
+ * 패키지(auth-ui)의 것이고, 오류를 알리는 쪽(@operato/graphql)의 것도 토스트의 것도 아니다.
79
+ * 그래서 방향은 `registerNotifyAssistant` 와 같다: 제공자가 자기 해석을 넣고, 토스트는 묻기만 한다.
80
+ *
81
+ * - `resolve(notification)` 이 `{ label, callback }` 을 주면 그 단추를, `null` 이면 아무것도 그리지 않는다.
82
+ * - **렌더 때마다 묻는다.** 값을 캐시하지 않는다. 던지면 「없음」으로 본다.
83
+ * - 같은 `key` 로 다시 등록하면 바꿔 끼우고, `null` 로 회수한다.
84
+ * - 그려지는 해석 단추는 **하나**다. 여럿이 답하면 등록 순서로 첫째만. 알림이 자기 `action` 을 가지면
85
+ * 해석 단추는 그리지 않는다. AI 도움 단추는 이것과 따로 있다.
86
+ *
87
+ * 제공자가 실린 앱에서만 단추가 생기므로, 눌러도 받는 쪽이 없는 단추는 생길 수 없다.
88
+ */
89
+ export type NotifyActionRequest = {
90
+ level: NotificationLevel;
91
+ message: string;
92
+ detail?: string;
93
+ ex?: any;
94
+ };
95
+ export type NotifyActionButton = {
96
+ label: string;
97
+ callback: () => void;
98
+ /**
99
+ * `quiet` - a button the person should press only after thinking: drawn without emphasis, so the notification's
100
+ * own sentence stays what is read first (the access request is one - things-factory ADR-0103). Default `normal`.
101
+ */
102
+ emphasis?: 'normal' | 'quiet';
103
+ };
104
+ export type NotifyActionResolver = (notification: NotifyActionRequest) => NotifyActionButton | null | undefined;
105
+ /** 제공자를 넣는다. 같은 key 는 바꿔 끼우고(등록 순서 자리는 처음 그대로), null 은 회수한다. */
106
+ export declare function registerNotifyAction(key: string, resolve: NotifyActionResolver | null): void;
107
+ /** 이 알림에 그릴 해석 단추 — 등록 순서로 처음 답한 것 하나. 던지거나 모양이 틀리면 그 제공자는 없는 것으로 본다. */
108
+ export declare function resolveNotifyAction(notification: NotifyActionRequest): NotifyActionButton | null;
74
109
  export declare function notify(notification: string | Notification): void;
75
110
  export type SnackbarAction = {
76
111
  type: 'OPEN_SNACKBAR' | 'CLOSE_SNACKBAR';
@@ -42,6 +42,29 @@ export function resolveNotifyAssistant() {
42
42
  }
43
43
  return provider.ask;
44
44
  }
45
+ const notifyActions = new Map();
46
+ /** 제공자를 넣는다. 같은 key 는 바꿔 끼우고(등록 순서 자리는 처음 그대로), null 은 회수한다. */
47
+ export function registerNotifyAction(key, resolve) {
48
+ if (resolve)
49
+ notifyActions.set(key, resolve);
50
+ else
51
+ notifyActions.delete(key);
52
+ }
53
+ /** 이 알림에 그릴 해석 단추 — 등록 순서로 처음 답한 것 하나. 던지거나 모양이 틀리면 그 제공자는 없는 것으로 본다. */
54
+ export function resolveNotifyAction(notification) {
55
+ for (const resolve of notifyActions.values()) {
56
+ let button;
57
+ try {
58
+ button = resolve(notification);
59
+ }
60
+ catch (_a) {
61
+ continue;
62
+ }
63
+ if (button && typeof button.label === 'string' && button.label && typeof button.callback === 'function')
64
+ return button;
65
+ }
66
+ return null;
67
+ }
45
68
  export function notify(notification) {
46
69
  if (typeof notification === 'string') {
47
70
  return notify({ message: notification });
@@ -1 +1 @@
1
- {"version":3,"file":"snackbar.js","sourceRoot":"","sources":["../../../src/actions/snackbar.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA;AAC5C,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAA;AAE9C,IAAI,aAAqB,CAAA;AA2BzB;;;;;GAKG;AACH,MAAM,aAAa,GAAsC;IACvD,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,CAAC,CAAC;CACV,CAAA;AA0CD,IAAI,eAAe,GAA2B,IAAI,CAAA;AAElD,mDAAmD;AACnD,MAAM,UAAU,uBAAuB,CAAC,SAAiC;IACvE,eAAe,GAAG,SAAS,CAAA;AAC7B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,QAAQ,GAAG,eAAe,CAAA;IAChC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,QAAQ,CAAA;IACnD,IAAI,OAAO,QAAQ,CAAC,GAAG,KAAK,UAAU;QAAE,OAAO,IAAI,CAAA;IAEnD,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE;gBAAE,OAAO,IAAI,CAAA;QACxC,CAAC;QAAC,WAAM,CAAC;YACP,uCAAuC;YACvC,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC,GAAG,CAAA;AACrB,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,YAAmC;IACxD,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAA;IAC1C,CAAC;IAED,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,YAA4B,CAAA;IAEzE,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAAC,QAAQ,EAAE;QACxB,MAAM,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE;KAC/C,CAAC,CACH,CAAA;AACH,CAAC;AAWD,MAAM,CAAC,MAAM,YAAY,GACvB,CACE,KAAwB,EACxB,EACE,OAAO,EACP,MAAM,EACN,EAAE,EACF,MAAM,EACN,KAAK,EAON,EACD,EAAE,CACJ,CAAC,QAAgD,EAAE,EAAE;;IACnD,QAAQ,CAAC;QACP,IAAI,EAAE,aAAa;QACnB,KAAK;QACL,OAAO;QACP,MAAM;QACN,EAAE;QACF,MAAM;KACP,CAAC,CAAA;IACF,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAA;IAElC,qCAAqC;IACrC,MAAM,SAAS,GAAG,MAAA,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,aAAa,CAAC,KAAK,CAAC,mCAAI,IAAI,CAAA;IAEvD,IAAI,SAAS,IAAI,CAAC,CAAC;QAAE,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,EAAE,SAAS,CAAC,CAAA;AAC7G,CAAC,CAAA","sourcesContent":["import { Action } from 'redux'\n\nexport const OPEN_SNACKBAR = 'OPEN_SNACKBAR'\nexport const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'\n\nvar snackbarTimer: number\n\n/**\n * 알림의 세 층\n * ------------\n * 이전에는 message 한 줄뿐이어서 \"무슨 일이 있었나 / 왜 / 기술적 원문\"이 한 문장에 섞였다.\n * 실제 호출 210건을 보면 18건이 err?.message 를 그대로 본문에 넣고 있었다.\n *\n * message 사용자가 읽을 한 줄. 무슨 일이 일어났는가.\n * detail 왜·어디서. 사용자가 다음 행동을 정하는 데 필요한 만큼.\n * ex 기술적 원문(예외·스택). 접힌 상태로 두고 필요한 사람만 펼친다.\n *\n * level 은 success 를 포함한다. 이전 타입에는 없어서(info|warn|error) 성공 알림이 레벨 없이\n * 호출되고 있었다 — 210건 중 117건이 그렇고 대부분 \"…successfully\" 문구다. 성공을 표현할\n * 자리가 없으니 개발자가 레벨을 비웠고, 결과적으로 성공과 무성격 알림이 같게 보였다.\n */\nexport type NotificationLevel = 'success' | 'info' | 'warn' | 'error'\nexport type Notification = {\n message: string\n level?: NotificationLevel\n /** 왜·어디서 — 한 단계 낮은 강조로 표시된다. */\n detail?: string\n option?: any\n /** 기술적 원문. 접힌 채로 전달되고, 펼치면 그대로 보인다. */\n ex?: any\n}\n\n/**\n * 레벨별 기본 표시 시간(ms).\n *\n * error 는 -1 = 자동으로 닫지 않는다. 이전에는 모든 레벨이 5초라, 사용자가 눈을 돌린 사이\n * 오류가 사라져 무엇이 잘못됐는지 알 수 없었다. 오류는 사람이 닫아야 사라지는 것이 맞다.\n */\nconst DEFAULT_TIMER: Record<NotificationLevel, number> = {\n success: 4000,\n info: 4000,\n warn: 8000,\n error: -1\n}\n\n/**\n * 알림에서 곧바로 AI 도움으로 넘어가는 연결점.\n *\n * 방향은 **AI 어시스턴트 → 토스트 주입**이다. 토스트는 AI 를 알지 못하고, 찾아 나서지도 않는다.\n * AI 를 제공하는 쪽이 \"지금 도울 수 있다\"고 판단했을 때 자기 함수를 넣어 준다.\n *\n * // AI 쪽에서\n * registerNotifyAssistant(req => openAssistant(req)) // 쓸 수 있게 되면 주입\n * registerNotifyAssistant(null) // 못 쓰게 되면 회수\n *\n * 가용 여부를 판단하는 책임도 주입하는 쪽에 있다. 설정 미비·권한 없음·백엔드 미응답처럼\n * \"AI 가 정상 동작하지 않는\" 상태를 아는 것은 AI 쪽이지 토스트가 아니다. 그런 상태가 되면\n * null 로 회수하면 되고, 그러면 토스트에서 버튼이 사라진다.\n *\n * 토스트는 주입된 것이 있는지만 본다. 그래서 AI 가 없는 앱, 설정이 죽은 앱에서는 이 기능이\n * 화면에 아예 나타나지 않는다 — 눌러도 아무 일 없는 버튼이나, 열면 \"쓸 수 없습니다\"가 뜨는\n * 버튼을 보여주지 않는다.\n */\nexport type NotifyAssistantRequest = {\n level: NotificationLevel\n message: string\n detail?: string\n ex?: any\n}\nexport type NotifyAssistantProvider = {\n ask: (request: NotifyAssistantRequest) => void\n /**\n * 지금 쓸 수 있는가. **렌더 시점마다** 호출되므로 값을 캐시하지 말고 그때의 상태를 답한다.\n *\n * 주입 후에 설정이 꺼지거나 세션이 끊기는 일이 있고, 그때 AI 쪽이 회수를 놓칠 수도 있다.\n * 이 함수가 있으면 회수를 놓쳐도 버튼이 뜨지 않는다 — 기존 앱에 주는 영향을 최소로 두기 위한\n * 안전장치다. 예외를 던지면 \"쓸 수 없음\"으로 본다.\n *\n * 생략하면 주입되어 있는 동안 항상 쓸 수 있다고 본다.\n */\n available?: () => boolean\n}\n/** 함수만 주입하는 형태도 받는다(가용 여부를 따로 알릴 것이 없는 제공자). */\nexport type NotifyAssistant = ((request: NotifyAssistantRequest) => void) | NotifyAssistantProvider\n\nlet notifyAssistant: NotifyAssistant | null = null\n\n/** AI 쪽에서 주입한다. null 을 넣으면 회수되고 토스트에서 버튼이 사라진다. */\nexport function registerNotifyAssistant(assistant: NotifyAssistant | null): void {\n notifyAssistant = assistant\n}\n\n/**\n * 지금 실제로 쓸 수 있는 도움 함수. 주입이 없거나 available() 이 거짓이면 null.\n * 토스트는 이 결과가 null 이 아닐 때만 버튼을 그린다.\n */\nexport function resolveNotifyAssistant(): ((request: NotifyAssistantRequest) => void) | null {\n const provider = notifyAssistant\n if (!provider) return null\n\n if (typeof provider === 'function') return provider\n if (typeof provider.ask !== 'function') return null\n\n if (provider.available) {\n try {\n if (!provider.available()) return null\n } catch {\n /* 가용 여부를 판단조차 못하는 상태면 쓸 수 없는 것으로 본다 */\n return null\n }\n }\n\n return provider.ask\n}\n\nexport function notify(notification: string | Notification): void {\n if (typeof notification === 'string') {\n return notify({ message: notification })\n }\n\n var { message, level, detail, option, ex } = notification as Notification\n\n document.dispatchEvent(\n new CustomEvent('notify', {\n detail: { message, level, detail, ex, option }\n })\n )\n}\n\nexport type SnackbarAction = {\n type: 'OPEN_SNACKBAR' | 'CLOSE_SNACKBAR'\n level?: NotificationLevel\n message?: string\n detail?: string\n ex?: any\n action?: any\n}\n\nexport const showSnackbar =\n (\n level: NotificationLevel,\n {\n message,\n detail,\n ex,\n action,\n timer\n }: {\n message: string\n detail?: string\n ex?: any\n action?: any\n timer?: number\n }\n ) =>\n (dispatch: (param: SnackbarAction) => Action<any>) => {\n dispatch({\n type: OPEN_SNACKBAR,\n level,\n message,\n detail,\n ex,\n action\n })\n window.clearTimeout(snackbarTimer)\n\n /* 호출자가 timer 를 주지 않으면 레벨별 기본값을 쓴다 */\n const effective = timer ?? DEFAULT_TIMER[level] ?? 4000\n\n if (effective != -1) snackbarTimer = window.setTimeout(() => dispatch({ type: CLOSE_SNACKBAR }), effective)\n }\n"]}
1
+ {"version":3,"file":"snackbar.js","sourceRoot":"","sources":["../../../src/actions/snackbar.ts"],"names":[],"mappings":"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","sourcesContent":["import { Action } from 'redux'\n\nexport const OPEN_SNACKBAR = 'OPEN_SNACKBAR'\nexport const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'\n\nvar snackbarTimer: number\n\n/**\n * 알림의 세 층\n * ------------\n * 이전에는 message 한 줄뿐이어서 \"무슨 일이 있었나 / 왜 / 기술적 원문\"이 한 문장에 섞였다.\n * 실제 호출 210건을 보면 18건이 err?.message 를 그대로 본문에 넣고 있었다.\n *\n * message 사용자가 읽을 한 줄. 무슨 일이 일어났는가.\n * detail 왜·어디서. 사용자가 다음 행동을 정하는 데 필요한 만큼.\n * ex 기술적 원문(예외·스택). 접힌 상태로 두고 필요한 사람만 펼친다.\n *\n * level 은 success 를 포함한다. 이전 타입에는 없어서(info|warn|error) 성공 알림이 레벨 없이\n * 호출되고 있었다 — 210건 중 117건이 그렇고 대부분 \"…successfully\" 문구다. 성공을 표현할\n * 자리가 없으니 개발자가 레벨을 비웠고, 결과적으로 성공과 무성격 알림이 같게 보였다.\n */\nexport type NotificationLevel = 'success' | 'info' | 'warn' | 'error'\nexport type Notification = {\n message: string\n level?: NotificationLevel\n /** 왜·어디서 — 한 단계 낮은 강조로 표시된다. */\n detail?: string\n option?: any\n /** 기술적 원문. 접힌 채로 전달되고, 펼치면 그대로 보인다. */\n ex?: any\n}\n\n/**\n * 레벨별 기본 표시 시간(ms).\n *\n * error 는 -1 = 자동으로 닫지 않는다. 이전에는 모든 레벨이 5초라, 사용자가 눈을 돌린 사이\n * 오류가 사라져 무엇이 잘못됐는지 알 수 없었다. 오류는 사람이 닫아야 사라지는 것이 맞다.\n */\nconst DEFAULT_TIMER: Record<NotificationLevel, number> = {\n success: 4000,\n info: 4000,\n warn: 8000,\n error: -1\n}\n\n/**\n * 알림에서 곧바로 AI 도움으로 넘어가는 연결점.\n *\n * 방향은 **AI 어시스턴트 → 토스트 주입**이다. 토스트는 AI 를 알지 못하고, 찾아 나서지도 않는다.\n * AI 를 제공하는 쪽이 \"지금 도울 수 있다\"고 판단했을 때 자기 함수를 넣어 준다.\n *\n * // AI 쪽에서\n * registerNotifyAssistant(req => openAssistant(req)) // 쓸 수 있게 되면 주입\n * registerNotifyAssistant(null) // 못 쓰게 되면 회수\n *\n * 가용 여부를 판단하는 책임도 주입하는 쪽에 있다. 설정 미비·권한 없음·백엔드 미응답처럼\n * \"AI 가 정상 동작하지 않는\" 상태를 아는 것은 AI 쪽이지 토스트가 아니다. 그런 상태가 되면\n * null 로 회수하면 되고, 그러면 토스트에서 버튼이 사라진다.\n *\n * 토스트는 주입된 것이 있는지만 본다. 그래서 AI 가 없는 앱, 설정이 죽은 앱에서는 이 기능이\n * 화면에 아예 나타나지 않는다 — 눌러도 아무 일 없는 버튼이나, 열면 \"쓸 수 없습니다\"가 뜨는\n * 버튼을 보여주지 않는다.\n */\nexport type NotifyAssistantRequest = {\n level: NotificationLevel\n message: string\n detail?: string\n ex?: any\n}\nexport type NotifyAssistantProvider = {\n ask: (request: NotifyAssistantRequest) => void\n /**\n * 지금 쓸 수 있는가. **렌더 시점마다** 호출되므로 값을 캐시하지 말고 그때의 상태를 답한다.\n *\n * 주입 후에 설정이 꺼지거나 세션이 끊기는 일이 있고, 그때 AI 쪽이 회수를 놓칠 수도 있다.\n * 이 함수가 있으면 회수를 놓쳐도 버튼이 뜨지 않는다 — 기존 앱에 주는 영향을 최소로 두기 위한\n * 안전장치다. 예외를 던지면 \"쓸 수 없음\"으로 본다.\n *\n * 생략하면 주입되어 있는 동안 항상 쓸 수 있다고 본다.\n */\n available?: () => boolean\n}\n/** 함수만 주입하는 형태도 받는다(가용 여부를 따로 알릴 것이 없는 제공자). */\nexport type NotifyAssistant = ((request: NotifyAssistantRequest) => void) | NotifyAssistantProvider\n\nlet notifyAssistant: NotifyAssistant | null = null\n\n/** AI 쪽에서 주입한다. null 을 넣으면 회수되고 토스트에서 버튼이 사라진다. */\nexport function registerNotifyAssistant(assistant: NotifyAssistant | null): void {\n notifyAssistant = assistant\n}\n\n/**\n * 지금 실제로 쓸 수 있는 도움 함수. 주입이 없거나 available() 이 거짓이면 null.\n * 토스트는 이 결과가 null 이 아닐 때만 버튼을 그린다.\n */\nexport function resolveNotifyAssistant(): ((request: NotifyAssistantRequest) => void) | null {\n const provider = notifyAssistant\n if (!provider) return null\n\n if (typeof provider === 'function') return provider\n if (typeof provider.ask !== 'function') return null\n\n if (provider.available) {\n try {\n if (!provider.available()) return null\n } catch {\n /* 가용 여부를 판단조차 못하는 상태면 쓸 수 없는 것으로 본다 */\n return null\n }\n }\n\n return provider.ask\n}\n\n/**\n * 알림에 단추를 하나 끼워 넣는 연결점 — 알림의 **뜻을 아는 쪽**이 주입한다.\n *\n * 토스트는 오류가 무엇을 뜻하는지 모른다. 권한 거절에서 「권한 요청」을 띄우는 일은 권한을 아는\n * 패키지(auth-ui)의 것이고, 오류를 알리는 쪽(@operato/graphql)의 것도 토스트의 것도 아니다.\n * 그래서 방향은 `registerNotifyAssistant` 와 같다: 제공자가 자기 해석을 넣고, 토스트는 묻기만 한다.\n *\n * - `resolve(notification)` 이 `{ label, callback }` 을 주면 그 단추를, `null` 이면 아무것도 그리지 않는다.\n * - **렌더 때마다 묻는다.** 값을 캐시하지 않는다. 던지면 「없음」으로 본다.\n * - 같은 `key` 로 다시 등록하면 바꿔 끼우고, `null` 로 회수한다.\n * - 그려지는 해석 단추는 **하나**다. 여럿이 답하면 등록 순서로 첫째만. 알림이 자기 `action` 을 가지면\n * 해석 단추는 그리지 않는다. AI 도움 단추는 이것과 따로 있다.\n *\n * 제공자가 실린 앱에서만 단추가 생기므로, 눌러도 받는 쪽이 없는 단추는 생길 수 없다.\n */\nexport type NotifyActionRequest = {\n level: NotificationLevel\n message: string\n detail?: string\n ex?: any\n}\nexport type NotifyActionButton = {\n label: string\n callback: () => void\n /**\n * `quiet` - a button the person should press only after thinking: drawn without emphasis, so the notification's\n * own sentence stays what is read first (the access request is one - things-factory ADR-0103). Default `normal`.\n */\n emphasis?: 'normal' | 'quiet'\n}\nexport type NotifyActionResolver = (notification: NotifyActionRequest) => NotifyActionButton | null | undefined\n\nconst notifyActions = new Map<string, NotifyActionResolver>()\n\n/** 제공자를 넣는다. 같은 key 는 바꿔 끼우고(등록 순서 자리는 처음 그대로), null 은 회수한다. */\nexport function registerNotifyAction(key: string, resolve: NotifyActionResolver | null): void {\n if (resolve) notifyActions.set(key, resolve)\n else notifyActions.delete(key)\n}\n\n/** 이 알림에 그릴 해석 단추 — 등록 순서로 처음 답한 것 하나. 던지거나 모양이 틀리면 그 제공자는 없는 것으로 본다. */\nexport function resolveNotifyAction(notification: NotifyActionRequest): NotifyActionButton | null {\n for (const resolve of notifyActions.values()) {\n let button: NotifyActionButton | null | undefined\n try {\n button = resolve(notification)\n } catch {\n continue\n }\n if (button && typeof button.label === 'string' && button.label && typeof button.callback === 'function') return button\n }\n return null\n}\n\nexport function notify(notification: string | Notification): void {\n if (typeof notification === 'string') {\n return notify({ message: notification })\n }\n\n var { message, level, detail, option, ex } = notification as Notification\n\n document.dispatchEvent(\n new CustomEvent('notify', {\n detail: { message, level, detail, ex, option }\n })\n )\n}\n\nexport type SnackbarAction = {\n type: 'OPEN_SNACKBAR' | 'CLOSE_SNACKBAR'\n level?: NotificationLevel\n message?: string\n detail?: string\n ex?: any\n action?: any\n}\n\nexport const showSnackbar =\n (\n level: NotificationLevel,\n {\n message,\n detail,\n ex,\n action,\n timer\n }: {\n message: string\n detail?: string\n ex?: any\n action?: any\n timer?: number\n }\n ) =>\n (dispatch: (param: SnackbarAction) => Action<any>) => {\n dispatch({\n type: OPEN_SNACKBAR,\n level,\n message,\n detail,\n ex,\n action\n })\n window.clearTimeout(snackbarTimer)\n\n /* 호출자가 timer 를 주지 않으면 레벨별 기본값을 쓴다 */\n const effective = timer ?? DEFAULT_TIMER[level] ?? 4000\n\n if (effective != -1) snackbarTimer = window.setTimeout(() => dispatch({ type: CLOSE_SNACKBAR }), effective)\n }\n"]}
@@ -3,7 +3,7 @@ import { LitElement } from 'lit';
3
3
  * A work surface in three panes: a list on the start side, the thing being worked on in the middle,
4
4
  * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).
5
5
  *
6
- * plant's station-screen modeller and figure's V3 editor built this grid apart - 220 | 1fr | 340 and
6
+ * plant's station-screen modeller and figure's modeller built this grid apart - 220 | 1fr | 340 and
7
7
  * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:
8
8
  *
9
9
  * - Slots `start` and `end`, and the default slot for the middle.
@@ -5,7 +5,7 @@ import { customElement, property } from 'lit/decorators.js';
5
5
  * A work surface in three panes: a list on the start side, the thing being worked on in the middle,
6
6
  * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).
7
7
  *
8
- * plant's station-screen modeller and figure's V3 editor built this grid apart - 220 | 1fr | 340 and
8
+ * plant's station-screen modeller and figure's modeller built this grid apart - 220 | 1fr | 340 and
9
9
  * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:
10
10
  *
11
11
  * - Slots `start` and `end`, and the default slot for the middle.
@@ -1 +1 @@
1
- {"version":3,"file":"ox-three-pane.js","sourceRoot":"","sources":["../../../src/components/ox-three-pane.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D;;;;;;;;;;;;;;GAcG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,UAAU;IAApC;;QAyFL,2DAA2D;QACU,cAAS,GAAG,KAAK,CAAA;QACtF,yDAAyD;QACU,YAAO,GAAG,KAAK,CAAA;IASpF,CAAC;IAPC,MAAM;QACJ,OAAO,IAAI,CAAA;;;;WAIJ,CAAA;IACT,CAAC;;AAnGM,kBAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFlB,AAtFY,CAsFZ;AAGoE;IAApE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;8CAAkB;AAEnB;IAAlE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAgB;AA5FvE,WAAW;IADvB,aAAa,CAAC,eAAe,CAAC;GAClB,WAAW,CAqGvB","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * A work surface in three panes: a list on the start side, the thing being worked on in the middle,\n * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).\n *\n * plant's station-screen modeller and figure's V3 editor built this grid apart - 220 | 1fr | 340 and\n * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:\n *\n * - Slots `start` and `end`, and the default slot for the middle.\n * - `fold-start` / `fold-end` take a side away; the middle takes its room. The toggles stay in the\n * screen's toolbar - the frame owns the layout, not the buttons.\n * - Side widths are `--three-pane-start-width` (200px) and `--three-pane-end-width` (340px). A screen\n * that needs other widths sets them; it does not write the grid again.\n * - It answers its own width, not the window's: narrower than 860px the sides give up some room\n * (160 / 300); narrower than 560px it is one column, the middle first, and it scrolls.\n */\n@customElement('ox-three-pane')\nexport class OxThreePane extends LitElement {\n static styles = css`\n :host {\n display: block;\n container-type: inline-size;\n min-height: 0;\n min-width: 0;\n }\n\n .panes {\n --_start: var(--three-pane-start-width, 200px);\n --_end: var(--three-pane-end-width, 340px);\n\n display: grid;\n grid-template-columns: var(--_start) minmax(0, 1fr) var(--_end);\n height: 100%;\n min-height: 0;\n }\n\n :host([fold-start]) .panes {\n --_start: 0px;\n }\n :host([fold-end]) .panes {\n --_end: 0px;\n }\n :host([fold-start]) .start,\n :host([fold-end]) .end {\n display: none;\n }\n\n .start,\n .middle,\n .end {\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n }\n /* Each pane is placed in its own column. Auto-placement would slide the middle into the start\n column's zero width once the start pane is folded away (display: none leaves the flow). */\n .start {\n grid-column: 1;\n border-right: 1px solid var(--md-sys-color-outline-variant);\n }\n .middle {\n grid-column: 2;\n }\n .end {\n grid-column: 3;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ::slotted(*) {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n\n @container (max-width: 859px) {\n .panes {\n --_start: var(--three-pane-start-width-narrow, 160px);\n --_end: var(--three-pane-end-width-narrow, 300px);\n }\n }\n\n @container (max-width: 559px) {\n .panes {\n grid-template-columns: minmax(0, 1fr);\n grid-template-rows: minmax(260px, 45%) auto auto;\n overflow-y: auto;\n }\n .start,\n .middle,\n .end {\n grid-column: 1;\n }\n .middle {\n order: -1;\n }\n .start {\n border-right: none;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n .end {\n border-left: none;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n }\n `\n\n /** Take the start side away; the middle takes its room. */\n @property({ type: Boolean, attribute: 'fold-start', reflect: true }) foldStart = false\n /** Take the end side away; the middle takes its room. */\n @property({ type: Boolean, attribute: 'fold-end', reflect: true }) foldEnd = false\n\n render() {\n return html`<div class=\"panes\">\n <div class=\"start\" part=\"start\"><slot name=\"start\"></slot></div>\n <div class=\"middle\" part=\"middle\"><slot></slot></div>\n <div class=\"end\" part=\"end\"><slot name=\"end\"></slot></div>\n </div>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-three-pane': OxThreePane\n }\n}\n"]}
1
+ {"version":3,"file":"ox-three-pane.js","sourceRoot":"","sources":["../../../src/components/ox-three-pane.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D;;;;;;;;;;;;;;GAcG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,UAAU;IAApC;;QAyFL,2DAA2D;QACU,cAAS,GAAG,KAAK,CAAA;QACtF,yDAAyD;QACU,YAAO,GAAG,KAAK,CAAA;IASpF,CAAC;IAPC,MAAM;QACJ,OAAO,IAAI,CAAA;;;;WAIJ,CAAA;IACT,CAAC;;AAnGM,kBAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFlB,AAtFY,CAsFZ;AAGoE;IAApE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;8CAAkB;AAEnB;IAAlE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAgB;AA5FvE,WAAW;IADvB,aAAa,CAAC,eAAe,CAAC;GAClB,WAAW,CAqGvB","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * A work surface in three panes: a list on the start side, the thing being worked on in the middle,\n * its details on the end side (ADR-0060 decision 8 and its 2026-09-24 addendum).\n *\n * plant's station-screen modeller and figure's modeller built this grid apart - 220 | 1fr | 340 and\n * 200 | 1fr | 340, each folding its sides from its own toolbar. The frame is that grid once:\n *\n * - Slots `start` and `end`, and the default slot for the middle.\n * - `fold-start` / `fold-end` take a side away; the middle takes its room. The toggles stay in the\n * screen's toolbar - the frame owns the layout, not the buttons.\n * - Side widths are `--three-pane-start-width` (200px) and `--three-pane-end-width` (340px). A screen\n * that needs other widths sets them; it does not write the grid again.\n * - It answers its own width, not the window's: narrower than 860px the sides give up some room\n * (160 / 300); narrower than 560px it is one column, the middle first, and it scrolls.\n */\n@customElement('ox-three-pane')\nexport class OxThreePane extends LitElement {\n static styles = css`\n :host {\n display: block;\n container-type: inline-size;\n min-height: 0;\n min-width: 0;\n }\n\n .panes {\n --_start: var(--three-pane-start-width, 200px);\n --_end: var(--three-pane-end-width, 340px);\n\n display: grid;\n grid-template-columns: var(--_start) minmax(0, 1fr) var(--_end);\n height: 100%;\n min-height: 0;\n }\n\n :host([fold-start]) .panes {\n --_start: 0px;\n }\n :host([fold-end]) .panes {\n --_end: 0px;\n }\n :host([fold-start]) .start,\n :host([fold-end]) .end {\n display: none;\n }\n\n .start,\n .middle,\n .end {\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n }\n /* Each pane is placed in its own column. Auto-placement would slide the middle into the start\n column's zero width once the start pane is folded away (display: none leaves the flow). */\n .start {\n grid-column: 1;\n border-right: 1px solid var(--md-sys-color-outline-variant);\n }\n .middle {\n grid-column: 2;\n }\n .end {\n grid-column: 3;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ::slotted(*) {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n\n @container (max-width: 859px) {\n .panes {\n --_start: var(--three-pane-start-width-narrow, 160px);\n --_end: var(--three-pane-end-width-narrow, 300px);\n }\n }\n\n @container (max-width: 559px) {\n .panes {\n grid-template-columns: minmax(0, 1fr);\n grid-template-rows: minmax(260px, 45%) auto auto;\n overflow-y: auto;\n }\n .start,\n .middle,\n .end {\n grid-column: 1;\n }\n .middle {\n order: -1;\n }\n .start {\n border-right: none;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n .end {\n border-left: none;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n }\n `\n\n /** Take the start side away; the middle takes its room. */\n @property({ type: Boolean, attribute: 'fold-start', reflect: true }) foldStart = false\n /** Take the end side away; the middle takes its room. */\n @property({ type: Boolean, attribute: 'fold-end', reflect: true }) foldEnd = false\n\n render() {\n return html`<div class=\"panes\">\n <div class=\"start\" part=\"start\"><slot name=\"start\"></slot></div>\n <div class=\"middle\" part=\"middle\"><slot></slot></div>\n <div class=\"end\" part=\"end\"><slot name=\"end\"></slot></div>\n </div>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-three-pane': OxThreePane\n }\n}\n"]}
@@ -7,7 +7,7 @@ import { customElement, property, state } from 'lit/decorators.js';
7
7
  import { i18next } from '@operato/i18n';
8
8
  import { SnackbarController } from '@operato/shell';
9
9
  import { ScrollbarStyles } from '@operato/styles';
10
- import { resolveNotifyAssistant } from '../actions/snackbar.js';
10
+ import { resolveNotifyAction, resolveNotifyAssistant } from '../actions/snackbar.js';
11
11
  let SnackBar = SnackBar_1 = class SnackBar extends LitElement {
12
12
  constructor() {
13
13
  super(...arguments);
@@ -156,6 +156,12 @@ let SnackBar = SnackBar_1 = class SnackBar extends LitElement {
156
156
  * 않는다. 선택해도 아무 일이 없는 버튼을 보여주지 않는다.
157
157
  */
158
158
  const assistant = resolveNotifyAssistant();
159
+ /*
160
+ * 알림의 뜻을 아는 패키지가 주입한 단추 하나(registerNotifyAction). 알림이 자기 action 을 가지면
161
+ * 그것이 이긴다 — 보낸 쪽이 정한 단추를 해석이 덮지 않는다.
162
+ */
163
+ const resolved = (action === null || action === void 0 ? void 0 : action.label) ? null : resolveNotifyAction({ level: tone, message, detail, ex });
164
+ const button = (action === null || action === void 0 ? void 0 : action.label) ? action : resolved;
159
165
  return html `
160
166
  <md-icon class=${tone} aria-hidden="true">${(_a = SnackBar_1.ICONS[tone]) !== null && _a !== void 0 ? _a : 'info'}</md-icon>
161
167
 
@@ -187,18 +193,19 @@ let SnackBar = SnackBar_1 = class SnackBar extends LitElement {
187
193
  `
188
194
  : nothing}
189
195
 
190
- ${(action === null || action === void 0 ? void 0 : action.label) || (assistant && (tone === 'error' || tone === 'warn'))
196
+ ${(button === null || button === void 0 ? void 0 : button.label) || (assistant && (tone === 'error' || tone === 'warn'))
191
197
  ? html `
192
198
  <div actions>
193
- ${(action === null || action === void 0 ? void 0 : action.label)
199
+ ${(button === null || button === void 0 ? void 0 : button.label)
194
200
  ? html `
195
201
  <md-text-button
202
+ ?quiet=${button.emphasis === 'quiet'}
196
203
  @click=${() => {
197
204
  var _a;
198
205
  this._snackbar.close();
199
- (_a = action.callback) === null || _a === void 0 ? void 0 : _a.call(action);
206
+ (_a = button.callback) === null || _a === void 0 ? void 0 : _a.call(button);
200
207
  }}
201
- >${action.label}</md-text-button
208
+ >${button.label}</md-text-button
202
209
  >
203
210
  `
204
211
  : nothing}
@@ -476,6 +483,15 @@ SnackBar.styles = css `
476
483
  --md-text-button-label-text-size: var(--md-sys-typescale-label-large-size, 0.875rem);
477
484
  }
478
485
 
486
+ /*
487
+ * A button to press only after thinking (emphasis 'quiet'): not in the level's accent, so the sentence above
488
+ * it is what the eye lands on. Still a button - it reads as one and is reached by keyboard like the others.
489
+ */
490
+ [actions] md-text-button[quiet] {
491
+ --md-text-button-label-text-color: var(--ox-snack-bar-supporting-color);
492
+ --md-text-button-label-text-weight: 400;
493
+ }
494
+
479
495
  /*
480
496
  * 레벨별 강조색 — 프레임워크 상태 색을 그대로 재사용한다.
481
497
  * 레벨은 호스트 속성으로 반영한다(:has() 에 의존하지 않는다).
@@ -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/text-button.js'\nimport '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { SnackbarController } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { resolveNotifyAssistant } from '../actions/snackbar.js'\n\n/** 복사 단추가 방금 무엇을 했는지. 잠깐 보여주고 원래 모습으로 돌아간다. */\ntype CopyState = 'idle' | 'done' | 'failed'\n\n@customElement('ox-snack-bar')\nclass SnackBar extends LitElement {\n static styles = css`\n /* 기술 원문 상자의 스크롤막대 — 앱 공통 모양을 그대로 쓴다 */\n ${ScrollbarStyles}\n\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 display: flex;\n align-items: center;\n gap: 4px;\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 /*\n * 원문 전체를 클립보드로. 상자가 접혀 있어도, 스크롤을 끝까지 내리지 않아도 **보이는 만큼이\n * 아니라 전부** 복사된다 — 오류를 옮겨 붙이는 사람이 실제로 원하는 것은 언제나 전문이다.\n * 그래서 요약줄에 둔다.\n */\n [technical] button[copy] {\n margin-inline-start: auto;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n border: none;\n border-radius: 4px;\n padding: 2px 5px;\n background-color: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1;\n cursor: pointer;\n }\n [technical] button[copy]:hover {\n color: var(--ox-snack-bar-color);\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 12%, transparent);\n }\n [technical] button[copy] md-icon {\n --md-icon-size: 14px;\n height: 14px;\n color: inherit;\n }\n [technical] pre {\n /*\n * 스크롤막대 색은 토스트 표면에서 파생시킨다. 공통 스타일의 기본값(--md-sys-color-outline)은\n * **현재 테마**의 색이라, 테마와 무관하게 어두운 이 표면 위에서는 밝기가 어긋난다.\n */\n --scrollbar-thumb-color: color-mix(in srgb, var(--ox-snack-bar-color) 28%, transparent);\n --scrollbar-thumb-hover-color: color-mix(in srgb, var(--ox-snack-bar-color) 48%, transparent);\n --scrollbar-thumb-border-radius: 3px;\n\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 disconnectedCallback() {\n super.disconnectedCallback()\n /* 다시 붙을 때 같은 핸들러가 두 벌 걸리지 않게 */\n this.removeEventListener('pointerdown', this.onDragStart)\n this.removeEventListener('pointermove', this.onDragMove)\n this.removeEventListener('pointerup', this.onDragEnd)\n clearTimeout(this.copyTimer)\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 /** 복사 단추가 방금 한 일. 잠깐 보였다가 스스로 지워진다. */\n @state() private copyState: CopyState = 'idle'\n private copyTimer?: number\n /** 어느 원문을 두고 위 상태가 남았는지 — 다른 알림이 뜨면 그 상태는 남의 것이다. */\n private copiedFrom?: string\n\n private async copyAll(event: Event, text: string) {\n /* 요약줄 안의 단추다. 복사하려다 상자가 접히거나 펼쳐지면 안 된다 */\n event.preventDefault()\n event.stopPropagation()\n\n /* currentTarget 은 처리가 끝나면 비므로 여기서 잡아 둔다 */\n const button = event.currentTarget as HTMLElement | null\n\n try {\n await navigator.clipboard.writeText(text)\n this.copyState = 'done'\n } catch {\n /* 안 됐으면 안 됐다고 보인다 — 조용히 넘어가면 붙여넣기 전까지 모른다 */\n this.copyState = 'failed'\n }\n\n this.copiedFrom = text\n\n /*\n * 몇 번이든 다시 복사할 수 있다. 그런데 「복사했습니다」가 아직 떠 있는 동안 또 누르면 글자가\n * 그대로라 아무 일도 안 일어난 것처럼 보인다 — 그래서 누를 때마다 단추를 한 번 깜박인다.\n * 상태로 처리하면 두 번째 누름이 같은 값이라 다시 그려지지 않는다.\n */\n button?.animate?.([{ opacity: 0.35 }, { opacity: 1 }], { duration: 220 })\n\n clearTimeout(this.copyTimer)\n this.copyTimer = window.setTimeout(() => {\n this.copyState = 'idle'\n }, 2000)\n }\n\n /** 이 원문에 대한 상태만 인정한다 — 앞 알림에서 남은 「복사했습니다」를 물려받지 않는다. */\n private copyStateFor(text: string): CopyState {\n return this.copiedFrom === text ? this.copyState : 'idle'\n }\n\n private copyIcon(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return 'check'\n case 'failed':\n return 'error'\n default:\n return 'content_copy'\n }\n }\n\n private copyLabel(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return i18next.t('text.copied')\n case 'failed':\n return i18next.t('text.copy failed')\n default:\n return i18next.t('text.copy all')\n }\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>\n ${i18next.t('text.technical detail')}\n <button\n copy\n type=\"button\"\n title=${this.copyLabel(exText)}\n @click=${(event: Event) => this.copyAll(event, exText)}\n >\n <md-icon aria-hidden=\"true\">${this.copyIcon(exText)}</md-icon>\n ${this.copyLabel(exText)}\n </button>\n </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('button.close', { defaultValue: '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"]}
1
+ {"version":3,"file":"ox-snack-bar.js","sourceRoot":"","sources":["../../../src/layouts/ox-snack-bar.ts"],"names":[],"mappings":";;AAAA,OAAO,qCAAqC,CAAA;AAC5C,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAMpF,IAAM,QAAQ,gBAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QAyQU,cAAS,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAA;QAEJ,WAAM,GAAY,KAAK,CAAA;QACnE,wCAAwC;QACG,UAAK,GAAW,MAAM,CAAA;QAGjE,iCAAiC;QACzB,eAAU,GAAkB,IAAI,CAAA;QAChC,UAAK,GAAW,CAAC,CAAA;QA2BjB,gBAAW,GAAG,CAAC,CAAe,EAAE,EAAE;YACxC,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,OAAM;YACxB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,OAAO,CAAA;YAC3B,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;YACd,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;QACnC,CAAC,CAAA;QAEO,eAAU,GAAG,CAAC,CAAe,EAAE,EAAE;YACvC,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI;gBAAE,OAAM;YACpC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,UAAU,CAAA;YACxC,sBAAsB;YACtB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAQ,CAAC,iBAAiB,GAAG,GAAG,CAAC,CAAC,CAAA;YAC1F,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QACrC,CAAC,CAAA;QAEO,cAAS,GAAG,GAAG,EAAE;YACvB,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI;gBAAE,OAAM;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAClC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;YACtB,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAA;YAEhC,IAAI,KAAK,IAAI,UAAQ,CAAC,iBAAiB,EAAE,CAAC;gBACxC,qBAAqB;gBACrB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAA;gBAC1E,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;oBACrB,gDAAgD;oBAChD,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;oBACjC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAA;oBACtB,qBAAqB,CAAC,GAAG,EAAE;wBACzB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;wBACjB,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAA;oBAClC,CAAC,CAAC,CAAA;gBACJ,CAAC,EAAE,GAAG,CAAC,CAAA;YACT,CAAC;iBAAM,CAAC;gBACN,mBAAmB;gBACnB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;YACnB,CAAC;QACH,CAAC,CAAA;QAUD,uCAAuC;QACtB,cAAS,GAAc,MAAM,CAAA;IA0LhD,CAAC;IAjQC,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,kEAAkE;QAClE,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QACtD,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACrD,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACpD,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,gCAAgC;QAChC,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QACzD,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACxD,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;QACrD,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC9B,CAAC;IAED,yBAAyB;IACjB,SAAS,CAAC,CAAS,EAAE,UAAkB,CAAC;QAC9C,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,mBAAmB,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;QACrD,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,oBAAoB,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;IAC5D,CAAC;IAuDO,KAAK,CAAC,OAAO,CAAC,KAAY,EAAE,IAAY;;QAC9C,0CAA0C;QAC1C,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,KAAK,CAAC,eAAe,EAAE,CAAA;QAEvB,2CAA2C;QAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,aAAmC,CAAA;QAExD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;YACzC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAA;QACzB,CAAC;QAAC,WAAM,CAAC;YACP,6CAA6C;YAC7C,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAA;QAC3B,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QAEtB;;;;WAIG;QACH,MAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,OAAO,uDAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAA;QAEzE,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC5B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACtC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAA;QACzB,CAAC,EAAE,IAAI,CAAC,CAAA;IACV,CAAC;IAED,wDAAwD;IAChD,YAAY,CAAC,IAAY;QAC/B,OAAO,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAA;IAC3D,CAAC;IAEO,QAAQ,CAAC,IAAY;QAC3B,QAAQ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,KAAK,MAAM;gBACT,OAAO,OAAO,CAAA;YAChB,KAAK,QAAQ;gBACX,OAAO,OAAO,CAAA;YAChB;gBACE,OAAO,cAAc,CAAA;QACzB,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,IAAY;QAC5B,QAAQ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,KAAK,MAAM;gBACT,OAAO,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;YACjC,KAAK,QAAQ;gBACX,OAAO,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAA;YACtC;gBACE,OAAO,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IAED,iEAAiE;IACzD,QAAQ,CAAC,EAAO;QACtB,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,CAAA;QAClB,IAAI,OAAO,EAAE,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAA;QACrC,IAAI,EAAE,YAAY,KAAK;YAAE,OAAO,EAAE,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,OAAO,EAAE,CAAA;QACvE,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACpC,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,MAAM,CAAC,EAAE,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;IAED,MAAM;;QACJ,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAA;QAC7D,MAAM,IAAI,GAAG,KAAK,IAAI,MAAM,CAAA;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAChC;;;WAGG;QACH,MAAM,SAAS,GAAG,sBAAsB,EAAE,CAAA;QAC1C;;;WAGG;QACH,MAAM,QAAQ,GAAG,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,EAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,IAAW,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;QACxG,MAAM,MAAM,GAAG,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,EAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;QAEhD,OAAO,IAAI,CAAA;uBACQ,IAAI,uBAAuB,MAAA,UAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,mCAAI,MAAM;;;;wBAIxD,OAAO;;;UAGrB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,mBAAmB,MAAM,QAAQ,CAAC,CAAC,CAAC,OAAO;;;UAGxD,MAAM;YACN,CAAC,CAAC,IAAI,CAAA;;;oBAGI,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;;;;4BAI1B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;6BACrB,CAAC,KAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;;kDAExB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;sBACjD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;;;uBAGrB,MAAM;;aAEhB;YACH,CAAC,CAAC,OAAO;;UAET,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,KAAI,CAAC,SAAS,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC;YACrE,CAAC,CAAC,IAAI,CAAA;;kBAEE,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK;gBACb,CAAC,CAAC,IAAI,CAAA;;iCAEU,MAAc,CAAC,QAAQ,KAAK,OAAO;iCACpC,GAAG,EAAE;;oBACZ,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAA;oBACtB,MAAA,MAAM,CAAC,QAAQ,sDAAI,CAAA;gBACrB,CAAC;2BACE,MAAM,CAAC,KAAK;;qBAElB;gBACH,CAAC,CAAC,OAAO;;;;;kBAKT,SAAS,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,MAAM,CAAC;gBAClD,CAAC,CAAC,IAAI,CAAA;;iCAES,GAAG,EAAE;oBACZ,SAAS,CAAC,EAAE,KAAK,EAAE,IAAW,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;oBACtD,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAA;gBACxB,CAAC;;;0BAGC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;;qBAE7B;gBACH,CAAC,CAAC,OAAO;;aAEd;YACH,CAAC,CAAC,OAAO;;;;;;;gBAOH,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;iBACnD,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE;;;KAGxC,CAAA;IACH,CAAC;IAED,YAAY,CAAC,KAAU;QACrB,IAAI,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAA;QAE7C,yBAAyB;QACzB,IAAI,cAAc,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;YACtB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;YACd,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAA;YAChC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;IAES,UAAU;QAClB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAA;QACnC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,MAAM,CAAA;IAC7C,CAAC;;AArhBM,eAAM,GAAG,GAAG,CAAA;;MAEf,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoQlB,AAtQY,CAsQZ;AAYD,2BAA2B;AACH,0BAAiB,GAAG,EAAE,AAAL,CAAK;AAgE9C,iCAAiC;AACT,cAAK,GAA2B;IACtD,OAAO,EAAE,cAAc;IACvB,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,OAAO;CACf,AAL4B,CAK5B;AA/E2C;IAA3C,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;wCAAwB;AAExB;IAA1C,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;uCAAuB;AACrC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wCAAY;AA+EtB;IAAhB,KAAK,EAAE;2CAAsC;AA7V1C,QAAQ;IADb,aAAa,CAAC,cAAc,CAAC;GACxB,QAAQ,CAuhBb","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, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { SnackbarController } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { resolveNotifyAction, resolveNotifyAssistant } from '../actions/snackbar.js'\n\n/** 복사 단추가 방금 무엇을 했는지. 잠깐 보여주고 원래 모습으로 돌아간다. */\ntype CopyState = 'idle' | 'done' | 'failed'\n\n@customElement('ox-snack-bar')\nclass SnackBar extends LitElement {\n static styles = css`\n /* 기술 원문 상자의 스크롤막대 — 앱 공통 모양을 그대로 쓴다 */\n ${ScrollbarStyles}\n\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 display: flex;\n align-items: center;\n gap: 4px;\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 /*\n * 원문 전체를 클립보드로. 상자가 접혀 있어도, 스크롤을 끝까지 내리지 않아도 **보이는 만큼이\n * 아니라 전부** 복사된다 — 오류를 옮겨 붙이는 사람이 실제로 원하는 것은 언제나 전문이다.\n * 그래서 요약줄에 둔다.\n */\n [technical] button[copy] {\n margin-inline-start: auto;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n border: none;\n border-radius: 4px;\n padding: 2px 5px;\n background-color: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--md-sys-typescale-label-small-size, 0.6875rem);\n line-height: 1;\n cursor: pointer;\n }\n [technical] button[copy]:hover {\n color: var(--ox-snack-bar-color);\n background-color: color-mix(in srgb, var(--ox-snack-bar-color) 12%, transparent);\n }\n [technical] button[copy] md-icon {\n --md-icon-size: 14px;\n height: 14px;\n color: inherit;\n }\n [technical] pre {\n /*\n * 스크롤막대 색은 토스트 표면에서 파생시킨다. 공통 스타일의 기본값(--md-sys-color-outline)은\n * **현재 테마**의 색이라, 테마와 무관하게 어두운 이 표면 위에서는 밝기가 어긋난다.\n */\n --scrollbar-thumb-color: color-mix(in srgb, var(--ox-snack-bar-color) 28%, transparent);\n --scrollbar-thumb-hover-color: color-mix(in srgb, var(--ox-snack-bar-color) 48%, transparent);\n --scrollbar-thumb-border-radius: 3px;\n\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 * A button to press only after thinking (emphasis 'quiet'): not in the level's accent, so the sentence above\n * it is what the eye lands on. Still a button - it reads as one and is reached by keyboard like the others.\n */\n [actions] md-text-button[quiet] {\n --md-text-button-label-text-color: var(--ox-snack-bar-supporting-color);\n --md-text-button-label-text-weight: 400;\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 disconnectedCallback() {\n super.disconnectedCallback()\n /* 다시 붙을 때 같은 핸들러가 두 벌 걸리지 않게 */\n this.removeEventListener('pointerdown', this.onDragStart)\n this.removeEventListener('pointermove', this.onDragMove)\n this.removeEventListener('pointerup', this.onDragEnd)\n clearTimeout(this.copyTimer)\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 /** 복사 단추가 방금 한 일. 잠깐 보였다가 스스로 지워진다. */\n @state() private copyState: CopyState = 'idle'\n private copyTimer?: number\n /** 어느 원문을 두고 위 상태가 남았는지 — 다른 알림이 뜨면 그 상태는 남의 것이다. */\n private copiedFrom?: string\n\n private async copyAll(event: Event, text: string) {\n /* 요약줄 안의 단추다. 복사하려다 상자가 접히거나 펼쳐지면 안 된다 */\n event.preventDefault()\n event.stopPropagation()\n\n /* currentTarget 은 처리가 끝나면 비므로 여기서 잡아 둔다 */\n const button = event.currentTarget as HTMLElement | null\n\n try {\n await navigator.clipboard.writeText(text)\n this.copyState = 'done'\n } catch {\n /* 안 됐으면 안 됐다고 보인다 — 조용히 넘어가면 붙여넣기 전까지 모른다 */\n this.copyState = 'failed'\n }\n\n this.copiedFrom = text\n\n /*\n * 몇 번이든 다시 복사할 수 있다. 그런데 「복사했습니다」가 아직 떠 있는 동안 또 누르면 글자가\n * 그대로라 아무 일도 안 일어난 것처럼 보인다 — 그래서 누를 때마다 단추를 한 번 깜박인다.\n * 상태로 처리하면 두 번째 누름이 같은 값이라 다시 그려지지 않는다.\n */\n button?.animate?.([{ opacity: 0.35 }, { opacity: 1 }], { duration: 220 })\n\n clearTimeout(this.copyTimer)\n this.copyTimer = window.setTimeout(() => {\n this.copyState = 'idle'\n }, 2000)\n }\n\n /** 이 원문에 대한 상태만 인정한다 — 앞 알림에서 남은 「복사했습니다」를 물려받지 않는다. */\n private copyStateFor(text: string): CopyState {\n return this.copiedFrom === text ? this.copyState : 'idle'\n }\n\n private copyIcon(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return 'check'\n case 'failed':\n return 'error'\n default:\n return 'content_copy'\n }\n }\n\n private copyLabel(text: string): string {\n switch (this.copyStateFor(text)) {\n case 'done':\n return i18next.t('text.copied')\n case 'failed':\n return i18next.t('text.copy failed')\n default:\n return i18next.t('text.copy all')\n }\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 * 알림의 뜻을 아는 패키지가 주입한 단추 하나(registerNotifyAction). 알림이 자기 action 을 가지면\n * 그것이 이긴다 — 보낸 쪽이 정한 단추를 해석이 덮지 않는다.\n */\n const resolved = action?.label ? null : resolveNotifyAction({ level: tone as any, message, detail, ex })\n const button = action?.label ? action : resolved\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>\n ${i18next.t('text.technical detail')}\n <button\n copy\n type=\"button\"\n title=${this.copyLabel(exText)}\n @click=${(event: Event) => this.copyAll(event, exText)}\n >\n <md-icon aria-hidden=\"true\">${this.copyIcon(exText)}</md-icon>\n ${this.copyLabel(exText)}\n </button>\n </summary>\n <pre>${exText}</pre>\n </details>\n `\n : nothing}\n\n ${button?.label || (assistant && (tone === 'error' || tone === 'warn'))\n ? html`\n <div actions>\n ${button?.label\n ? html`\n <md-text-button\n ?quiet=${(button as any).emphasis === 'quiet'}\n @click=${() => {\n this._snackbar.close()\n button.callback?.()\n }}\n >${button.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('button.close', { defaultValue: '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"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { registerNotifyAction, resolveNotifyAction } from '../src/actions/snackbar.js';
3
+ /**
4
+ * A package that knows what a notification means puts one button on it (things-factory ADR-0103 decision 5).
5
+ * The toast asks at every render, keeps nothing, and draws one button at most.
6
+ */
7
+ const ERROR = { level: 'error', message: 'refused', ex: [{ extensions: { because: 'role' } }] };
8
+ describe('registerNotifyAction', () => {
9
+ afterEach(() => {
10
+ registerNotifyAction('first', null);
11
+ registerNotifyAction('second', null);
12
+ });
13
+ it('★ a provider that answers puts its button on', () => {
14
+ var _a;
15
+ registerNotifyAction('first', n => { var _a, _b, _c; return (((_c = (_b = (_a = n.ex) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.extensions) === null || _c === void 0 ? void 0 : _c.because) === 'role' ? { label: 'request access', callback: () => { } } : null); });
16
+ expect((_a = resolveNotifyAction(ERROR)) === null || _a === void 0 ? void 0 : _a.label).to.equal('request access');
17
+ expect(resolveNotifyAction({ level: 'error', message: 'other' })).to.equal(null);
18
+ });
19
+ it('★ one button - the first registered that answers', () => {
20
+ var _a;
21
+ registerNotifyAction('first', () => ({ label: 'one', callback: () => { } }));
22
+ registerNotifyAction('second', () => ({ label: 'two', callback: () => { } }));
23
+ expect((_a = resolveNotifyAction(ERROR)) === null || _a === void 0 ? void 0 : _a.label).to.equal('one');
24
+ });
25
+ it('★ a provider that throws counts as none - the next one still answers', () => {
26
+ var _a;
27
+ registerNotifyAction('first', () => {
28
+ throw new Error('broken');
29
+ });
30
+ registerNotifyAction('second', () => ({ label: 'two', callback: () => { } }));
31
+ expect((_a = resolveNotifyAction(ERROR)) === null || _a === void 0 ? void 0 : _a.label).to.equal('two');
32
+ });
33
+ it('the same key replaces, null takes it back', () => {
34
+ var _a;
35
+ registerNotifyAction('first', () => ({ label: 'old', callback: () => { } }));
36
+ registerNotifyAction('first', () => ({ label: 'new', callback: () => { } }));
37
+ expect((_a = resolveNotifyAction(ERROR)) === null || _a === void 0 ? void 0 : _a.label).to.equal('new');
38
+ registerNotifyAction('first', null);
39
+ expect(resolveNotifyAction(ERROR)).to.equal(null);
40
+ });
41
+ it('asked every time, never kept - a provider that changes its mind is heard', () => {
42
+ var _a;
43
+ let on = true;
44
+ registerNotifyAction('first', () => (on ? { label: 'now', callback: () => { } } : null));
45
+ expect((_a = resolveNotifyAction(ERROR)) === null || _a === void 0 ? void 0 : _a.label).to.equal('now');
46
+ on = false;
47
+ expect(resolveNotifyAction(ERROR)).to.equal(null);
48
+ });
49
+ it('a malformed answer is no button', () => {
50
+ registerNotifyAction('first', () => ({ label: '', callback: () => { } }));
51
+ expect(resolveNotifyAction(ERROR)).to.equal(null);
52
+ });
53
+ });
54
+ //# sourceMappingURL=notify-action.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify-action.test.js","sourceRoot":"","sources":["../../test/notify-action.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAEtF;;;GAGG;AAEH,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,OAAgB,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAA;AAExG,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,SAAS,CAAC,GAAG,EAAE;QACb,oBAAoB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QACnC,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;;QACtD,oBAAoB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,mBAAC,OAAA,CAAC,CAAA,MAAA,MAAA,MAAA,CAAC,CAAC,EAAE,0CAAG,CAAC,CAAC,0CAAE,UAAU,0CAAE,OAAO,MAAK,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA,EAAA,CAAC,CAAA;QACxI,MAAM,CAAC,MAAA,mBAAmB,CAAC,KAAK,CAAC,0CAAE,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;QACpE,MAAM,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAClF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;;QAC1D,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC3E,oBAAoB,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5E,MAAM,CAAC,MAAA,mBAAmB,CAAC,KAAK,CAAC,0CAAE,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;;QAC9E,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE;YACjC,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC3B,CAAC,CAAC,CAAA;QACF,oBAAoB,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5E,MAAM,CAAC,MAAA,mBAAmB,CAAC,KAAK,CAAC,0CAAE,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;;QACnD,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC3E,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC3E,MAAM,CAAC,MAAA,mBAAmB,CAAC,KAAK,CAAC,0CAAE,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACzD,oBAAoB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QACnC,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;;QAClF,IAAI,EAAE,GAAG,IAAI,CAAA;QACb,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QACvF,MAAM,CAAC,MAAA,mBAAmB,CAAC,KAAK,CAAC,0CAAE,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACzD,EAAE,GAAG,KAAK,CAAA;QACV,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,oBAAoB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QACxE,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { expect } from '@open-wc/testing'\n\nimport { registerNotifyAction, resolveNotifyAction } from '../src/actions/snackbar.js'\n\n/**\n * A package that knows what a notification means puts one button on it (things-factory ADR-0103 decision 5).\n * The toast asks at every render, keeps nothing, and draws one button at most.\n */\n\nconst ERROR = { level: 'error' as const, message: 'refused', ex: [{ extensions: { because: 'role' } }] }\n\ndescribe('registerNotifyAction', () => {\n afterEach(() => {\n registerNotifyAction('first', null)\n registerNotifyAction('second', null)\n })\n\n it('★ a provider that answers puts its button on', () => {\n registerNotifyAction('first', n => (n.ex?.[0]?.extensions?.because === 'role' ? { label: 'request access', callback: () => {} } : null))\n expect(resolveNotifyAction(ERROR)?.label).to.equal('request access')\n expect(resolveNotifyAction({ level: 'error', message: 'other' })).to.equal(null)\n })\n\n it('★ one button - the first registered that answers', () => {\n registerNotifyAction('first', () => ({ label: 'one', callback: () => {} }))\n registerNotifyAction('second', () => ({ label: 'two', callback: () => {} }))\n expect(resolveNotifyAction(ERROR)?.label).to.equal('one')\n })\n\n it('★ a provider that throws counts as none - the next one still answers', () => {\n registerNotifyAction('first', () => {\n throw new Error('broken')\n })\n registerNotifyAction('second', () => ({ label: 'two', callback: () => {} }))\n expect(resolveNotifyAction(ERROR)?.label).to.equal('two')\n })\n\n it('the same key replaces, null takes it back', () => {\n registerNotifyAction('first', () => ({ label: 'old', callback: () => {} }))\n registerNotifyAction('first', () => ({ label: 'new', callback: () => {} }))\n expect(resolveNotifyAction(ERROR)?.label).to.equal('new')\n registerNotifyAction('first', null)\n expect(resolveNotifyAction(ERROR)).to.equal(null)\n })\n\n it('asked every time, never kept - a provider that changes its mind is heard', () => {\n let on = true\n registerNotifyAction('first', () => (on ? { label: 'now', callback: () => {} } : null))\n expect(resolveNotifyAction(ERROR)?.label).to.equal('now')\n on = false\n expect(resolveNotifyAction(ERROR)).to.equal(null)\n })\n\n it('a malformed answer is no button', () => {\n registerNotifyAction('first', () => ({ label: '', callback: () => {} }))\n expect(resolveNotifyAction(ERROR)).to.equal(null)\n })\n})\n"]}