@things-factory/auth-ui 10.1.4 → 10.1.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.
Files changed (59) hide show
  1. package/dist-client/components/change-password.d.ts +11 -0
  2. package/dist-client/components/change-password.js +103 -6
  3. package/dist-client/components/change-password.js.map +1 -1
  4. package/dist-client/components/role-privilege-picker.d.ts +37 -0
  5. package/dist-client/components/role-privilege-picker.js +58 -0
  6. package/dist-client/components/role-privilege-picker.js.map +1 -0
  7. package/dist-client/components/role-template-picker.d.ts +83 -0
  8. package/dist-client/components/role-template-picker.js +487 -0
  9. package/dist-client/components/role-template-picker.js.map +1 -0
  10. package/dist-client/components/user-detail.d.ts +151 -0
  11. package/dist-client/components/user-detail.js +631 -0
  12. package/dist-client/components/user-detail.js.map +1 -0
  13. package/dist-client/components/user-invite.d.ts +80 -0
  14. package/dist-client/components/user-invite.js +422 -0
  15. package/dist-client/components/user-invite.js.map +1 -0
  16. package/dist-client/entries/auth/activate.d.ts +0 -1
  17. package/dist-client/entries/auth/activate.js +0 -8
  18. package/dist-client/entries/auth/activate.js.map +1 -1
  19. package/dist-client/pages/role/role-list-page.d.ts +203 -0
  20. package/dist-client/pages/role/role-list-page.js +838 -0
  21. package/dist-client/pages/role/role-list-page.js.map +1 -0
  22. package/dist-client/pages/role/role-privilege-page.d.ts +181 -0
  23. package/dist-client/pages/role/role-privilege-page.js +980 -0
  24. package/dist-client/pages/role/role-privilege-page.js.map +1 -0
  25. package/dist-client/pages/user/user-list-page.d.ts +88 -0
  26. package/dist-client/pages/user/user-list-page.js +474 -0
  27. package/dist-client/pages/user/user-list-page.js.map +1 -0
  28. package/dist-client/route.js +5 -2
  29. package/dist-client/route.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +4 -4
  33. package/things-factory.config.js +3 -2
  34. package/translations/en.json +72 -3
  35. package/translations/ja.json +14 -3
  36. package/translations/ko.json +73 -4
  37. package/translations/ms.json +15 -4
  38. package/translations/zh.json +14 -3
  39. package/dist-client/components/contact-us.d.ts +0 -20
  40. package/dist-client/components/contact-us.js +0 -121
  41. package/dist-client/components/contact-us.js.map +0 -1
  42. package/dist-client/components/create-role.d.ts +0 -1
  43. package/dist-client/components/create-role.js +0 -117
  44. package/dist-client/components/create-role.js.map +0 -1
  45. package/dist-client/components/credential-manager.d.ts +0 -11
  46. package/dist-client/components/credential-manager.js +0 -64
  47. package/dist-client/components/credential-manager.js.map +0 -1
  48. package/dist-client/components/role-edit-form.d.ts +0 -1
  49. package/dist-client/components/role-edit-form.js +0 -95
  50. package/dist-client/components/role-edit-form.js.map +0 -1
  51. package/dist-client/components/role-privilege-editor.d.ts +0 -1
  52. package/dist-client/components/role-privilege-editor.js +0 -368
  53. package/dist-client/components/role-privilege-editor.js.map +0 -1
  54. package/dist-client/pages/role/role-management.d.ts +0 -4
  55. package/dist-client/pages/role/role-management.js +0 -137
  56. package/dist-client/pages/role/role-management.js.map +0 -1
  57. package/dist-client/pages/user/user-management.d.ts +0 -35
  58. package/dist-client/pages/user/user-management.js +0 -223
  59. package/dist-client/pages/user/user-management.js.map +0 -1
@@ -14,6 +14,17 @@ export declare class ChangePassword extends ChangePassword_base {
14
14
  looseCharacterLength?: number;
15
15
  };
16
16
  form: HTMLFormElement;
17
+ /**
18
+ * Set once the change went through.
19
+ *
20
+ * The server answers with a message and the shell shows it as a toast, but a toast leaves and
21
+ * the same three empty boxes stay behind. A person who has just changed their password reads
22
+ * that as "nothing happened" — the report that brought this in said exactly that.
23
+ *
24
+ * So the form is replaced by a statement that stays. It also stops the boxes from inviting a
25
+ * second attempt at something already done.
26
+ */
27
+ private changed;
17
28
  private passwordPattern;
18
29
  private passwordHelp;
19
30
  render(): import("lit-html").TemplateResult<1>;
@@ -1,6 +1,6 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import { css, html, LitElement } from 'lit';
3
- import { customElement, property, query } from 'lit/decorators.js';
3
+ import { customElement, property, query, state } from 'lit/decorators.js';
4
4
  import { i18next, localize } from '@operato/i18n';
5
5
  import { FormFieldStyles } from '@operato/styles';
6
6
  import { auth } from '@things-factory/auth-base/dist-client/auth.js';
@@ -9,6 +9,17 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
9
9
  constructor() {
10
10
  super(...arguments);
11
11
  this.passwordRule = {};
12
+ /**
13
+ * Set once the change went through.
14
+ *
15
+ * The server answers with a message and the shell shows it as a toast, but a toast leaves and
16
+ * the same three empty boxes stay behind. A person who has just changed their password reads
17
+ * that as "nothing happened" — the report that brought this in said exactly that.
18
+ *
19
+ * So the form is replaced by a statement that stays. It also stops the boxes from inviting a
20
+ * second attempt at something already done.
21
+ */
22
+ this.changed = false;
12
23
  this.passwordPattern = '';
13
24
  this.passwordHelp = '';
14
25
  }
@@ -73,6 +84,40 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
73
84
  }
74
85
 
75
86
  /* 비밀번호 규칙 안내문 — 본문보다 낮은 강조. 라이트/다크 모두 읽히도록 토큰 파생. */
87
+ /* The statement that replaces the form once the change took. */
88
+ .changed {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 8px;
92
+ /*
93
+ * 같은 여백을 위쪽 인풋과 나눠 쓴다. 폼 상태에서는 첫 인풋의 margin 이 「비밀번호」
94
+ * 라벨과의 간격을 만드는데, 이 문장은 margin 이 없어 라벨에 붙어 있었다 — 같은 자리에
95
+ * 두 가지 간격이 생겼다. 값을 새로 정하지 않고 인풋과 같은 토큰을 쓴다.
96
+ */
97
+ margin: var(--change-password-field-margin, var(--spacing-small) 0);
98
+ padding: 12px 14px;
99
+ border: 1px solid var(--md-sys-color-outline-variant);
100
+ border-left: 3px solid var(--md-sys-color-primary);
101
+ border-radius: 7px;
102
+ background: var(--md-sys-color-surface-variant);
103
+ color: var(--md-sys-color-on-surface-variant);
104
+ line-height: 1.5;
105
+ }
106
+
107
+ .changed md-icon {
108
+ color: var(--md-sys-color-primary);
109
+ }
110
+
111
+ .changed button {
112
+ margin-left: auto;
113
+ border: 1px solid var(--md-sys-color-outline-variant);
114
+ background: transparent;
115
+ color: inherit;
116
+ border-radius: 7px;
117
+ padding: 5px 11px;
118
+ cursor: pointer;
119
+ }
120
+
76
121
  .helper-text {
77
122
  font-size: 12px;
78
123
  color: var(--md-sys-color-on-surface-variant);
@@ -83,12 +128,32 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
83
128
  `
84
129
  ]; }
85
130
  render() {
131
+ if (this.changed) {
132
+ return html `
133
+ <div class="changed" role="status">
134
+ <md-icon>check_circle</md-icon>
135
+ <span>${i18next.t('text.password changed successfully')}</span>
136
+ <button type="button" @click=${() => (this.changed = false)}>
137
+ ${i18next.t('text.change password')}
138
+ </button>
139
+ </div>
140
+ `;
141
+ }
86
142
  return html `
87
143
  <form>
88
144
  <div class="field">
89
145
  <input type="password" name="current_pass" placeholder=${i18next.t('text.current password')} required />
90
146
  </div>
91
- <span id="password-helper" class="helper-text">${this.passwordHelp}</span>
147
+ <!--
148
+ The rule belongs under the field the rule applies to.
149
+
150
+ It used to sit between the two, directly beneath the current password, so a sighted
151
+ reader took it as the rule for that field — and the current password has no reason to
152
+ satisfy today's rule. It may predate it. The server only checks that it matches
153
+ (change-pwd.ts verifies the current one and validates the new one against the rule).
154
+ aria-describedby always pointed at the new field, so a screen reader had it right and
155
+ only the eye was misled.
156
+ -->
92
157
  <div class="field">
93
158
  <input
94
159
  type="password"
@@ -99,11 +164,25 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
99
164
  aria-describedby="password-helper"
100
165
  />
101
166
  </div>
167
+ <span id="password-helper" class="helper-text">${this.passwordHelp}</span>
102
168
  <div class="field">
103
169
  <input type="password" name="confirm_pass" placeholder=${i18next.t('text.confirm password')} required />
104
170
  </div>
105
171
 
106
- <md-elevated-button @click=${this.submit.bind(this)}>${i18next.t('text.change password')}</md-elevated-button>
172
+ <!--
173
+ type="button", the way abstract-sign and abstract-auth-page already declare it.
174
+
175
+ A Material button inside a form defaults to type="submit" and submits unless the click
176
+ is default-prevented. So one click ran submit() and then let the browser submit the
177
+ form as well. submit() had already called form.reset(), so constraint validation found
178
+ three empty required fields, rejected the first one, and put the caret back on the
179
+ current password with an English "Please fill out this field." bubble on a Korean
180
+ screen. It read as the current password being refused.
181
+ [확인함] 2026-09-09 · 사용자 보고 · 화면 사진
182
+ -->
183
+ <md-elevated-button type="button" @click=${this.submit.bind(this)}
184
+ >${i18next.t('text.change password')}</md-elevated-button
185
+ >
107
186
  </form>
108
187
  `;
109
188
  }
@@ -130,8 +209,11 @@ let ChangePassword = class ChangePassword extends localize(i18next)(LitElement)
130
209
  if (params['new_pass'] !== params['confirm_pass']) {
131
210
  return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'));
132
211
  }
133
- auth.changePassword(params);
134
- this.form.reset();
212
+ /* Clear only once it took — a refused change used to wipe all three fields. */
213
+ if (await auth.changePassword(params)) {
214
+ this.form.reset();
215
+ this.changed = true;
216
+ }
135
217
  }
136
218
  showToast(message) {
137
219
  document.dispatchEvent(new CustomEvent('notify', { detail: { message } }));
@@ -143,8 +225,23 @@ __decorate([
143
225
  ], ChangePassword.prototype, "passwordRule", void 0);
144
226
  __decorate([
145
227
  query('form'),
146
- __metadata("design:type", HTMLFormElement)
228
+ __metadata("design:type", HTMLFormElement
229
+ /**
230
+ * Set once the change went through.
231
+ *
232
+ * The server answers with a message and the shell shows it as a toast, but a toast leaves and
233
+ * the same three empty boxes stay behind. A person who has just changed their password reads
234
+ * that as "nothing happened" — the report that brought this in said exactly that.
235
+ *
236
+ * So the form is replaced by a statement that stays. It also stops the boxes from inviting a
237
+ * second attempt at something already done.
238
+ */
239
+ )
147
240
  ], ChangePassword.prototype, "form", void 0);
241
+ __decorate([
242
+ state(),
243
+ __metadata("design:type", Object)
244
+ ], ChangePassword.prototype, "changed", void 0);
148
245
  ChangePassword = __decorate([
149
246
  customElement('change-password')
150
247
  ], ChangePassword);
@@ -1 +1 @@
1
- {"version":3,"file":"change-password.js","sourceRoot":"","sources":["../../client/components/change-password.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { FormFieldStyles } from '@operato/styles'\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js'\n\n@customElement('change-password')\nexport class ChangePassword extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n css`\n * {\n box-sizing: border-box;\n }\n\n *:focus {\n outline: none;\n }\n\n form {\n display: flex;\n flex-direction: column;\n }\n\n /*\n * 색·테두리·모서리·포커스·placeholder 색은 FormFieldStyles 가 담당한다.\n * 여기서는 이 화면의 치수만 둔다 — 같은 프로필 화면의 위쪽 인풋(profile-component)과\n * 같은 값을 써서 한 화면에 두 가지 인상이 생기지 않게 한다.\n */\n input {\n border-radius: var(--change-password-field-border-radius, var(--border-radius));\n margin: var(--change-password-field-margin, var(--spacing-small) 0);\n padding: var(--change-password-field-padding, 9px);\n font: var(--change-password-field-font, var(--auth-input-field-font));\n width: var(--change-password-field-width, var(--auth-input-field-width));\n }\n\n ::placeholder {\n /*\n * 크기를 줄이지 않는다. 이전에는 0.8rem 이라 위쪽 인풋의 값(16px)보다 작게 보여\n * 같은 화면에서 패스워드 칸만 다른 스타일처럼 느껴졌다.\n */\n text-transform: capitalize;\n }\n\n md-elevated-button {\n margin: var(--spacing-small) auto var(--spacing-medium) auto;\n text-transform: capitalize;\n }\n\n button {\n /* fallback 도 테마 토큰으로 — 하드코딩 색은 다크에서 어긋난다 */\n background-color: var(--secondary-color, var(--md-sys-color-secondary));\n margin: 2px 2px 10px 2px;\n height: var(--button-height, 28px);\n color: var(--button-color, var(--md-sys-color-on-secondary));\n font: var(--button-font);\n border-radius: var(--button-radius, var(--md-sys-shape-corner-small, 5px));\n border: var(--button-border, 1px solid transparent);\n line-height: 1.5;\n }\n\n button:hover,\n button:active {\n background-color: var(--button-active-background-color, var(--md-sys-color-primary));\n border: var(--button-active-border);\n }\n\n /* 비밀번호 규칙 안내문 — 본문보다 낮은 강조. 라이트/다크 모두 읽히도록 토큰 파생. */\n .helper-text {\n font-size: 12px;\n color: var(--md-sys-color-on-surface-variant);\n margin-top: 4px;\n display: block; /* 텍스트를 입력 필드 아래에 배치 */\n line-height: 1.5; /* 텍스트 줄 간격 조절 */\n }\n `\n ]\n\n @property({ type: Object }) passwordRule: {\n lowerCase?: boolean\n upperCase?: boolean\n digit?: boolean\n specialCharacter?: boolean\n allowRepeat?: boolean\n useTightPattern?: boolean\n useLoosePattern?: boolean\n tightCharacterLength?: number\n looseCharacterLength?: number\n } = {}\n\n @query('form') form!: HTMLFormElement\n\n private passwordPattern: string = ''\n private passwordHelp: string = ''\n\n render() {\n return html`\n <form>\n <div class=\"field\">\n <input type=\"password\" name=\"current_pass\" placeholder=${i18next.t('text.current password')} required />\n </div>\n <span id=\"password-helper\" class=\"helper-text\">${this.passwordHelp}</span>\n <div class=\"field\">\n <input\n type=\"password\"\n name=\"new_pass\"\n placeholder=${i18next.t('text.new password')}\n required\n pattern=${this.passwordPattern}\n aria-describedby=\"password-helper\"\n />\n </div>\n <div class=\"field\">\n <input type=\"password\" name=\"confirm_pass\" placeholder=${i18next.t('text.confirm password')} required />\n </div>\n\n <md-elevated-button @click=${this.submit.bind(this)}>${i18next.t('text.change password')}</md-elevated-button>\n </form>\n `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('passwordRule')) {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n }\n\n languageUpdated() {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n\n async submit() {\n const formData = new FormData(this.form)\n let params = {}\n for (const [key, value] of formData.entries()) {\n if (!value) {\n const placeholder = (this.form.querySelector(`[name=${key}]`) as HTMLInputElement)?.placeholder\n return this.showToast(i18next.t('error.value is empty', { value: placeholder || key }))\n }\n params[key] = value\n }\n\n if (params['new_pass'] !== params['confirm_pass']) {\n return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'))\n }\n\n auth.changePassword(params)\n this.form.reset()\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
1
+ {"version":3,"file":"change-password.js","sourceRoot":"","sources":["../../client/components/change-password.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { FormFieldStyles } from '@operato/styles'\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js'\n\n@customElement('change-password')\nexport class ChangePassword extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n css`\n * {\n box-sizing: border-box;\n }\n\n *:focus {\n outline: none;\n }\n\n form {\n display: flex;\n flex-direction: column;\n }\n\n /*\n * 색·테두리·모서리·포커스·placeholder 색은 FormFieldStyles 가 담당한다.\n * 여기서는 이 화면의 치수만 둔다 — 같은 프로필 화면의 위쪽 인풋(profile-component)과\n * 같은 값을 써서 한 화면에 두 가지 인상이 생기지 않게 한다.\n */\n input {\n border-radius: var(--change-password-field-border-radius, var(--border-radius));\n margin: var(--change-password-field-margin, var(--spacing-small) 0);\n padding: var(--change-password-field-padding, 9px);\n font: var(--change-password-field-font, var(--auth-input-field-font));\n width: var(--change-password-field-width, var(--auth-input-field-width));\n }\n\n ::placeholder {\n /*\n * 크기를 줄이지 않는다. 이전에는 0.8rem 이라 위쪽 인풋의 값(16px)보다 작게 보여\n * 같은 화면에서 패스워드 칸만 다른 스타일처럼 느껴졌다.\n */\n text-transform: capitalize;\n }\n\n md-elevated-button {\n margin: var(--spacing-small) auto var(--spacing-medium) auto;\n text-transform: capitalize;\n }\n\n button {\n /* fallback 도 테마 토큰으로 — 하드코딩 색은 다크에서 어긋난다 */\n background-color: var(--secondary-color, var(--md-sys-color-secondary));\n margin: 2px 2px 10px 2px;\n height: var(--button-height, 28px);\n color: var(--button-color, var(--md-sys-color-on-secondary));\n font: var(--button-font);\n border-radius: var(--button-radius, var(--md-sys-shape-corner-small, 5px));\n border: var(--button-border, 1px solid transparent);\n line-height: 1.5;\n }\n\n button:hover,\n button:active {\n background-color: var(--button-active-background-color, var(--md-sys-color-primary));\n border: var(--button-active-border);\n }\n\n /* 비밀번호 규칙 안내문 — 본문보다 낮은 강조. 라이트/다크 모두 읽히도록 토큰 파생. */\n /* The statement that replaces the form once the change took. */\n .changed {\n display: flex;\n align-items: center;\n gap: 8px;\n /*\n * 같은 여백을 위쪽 인풋과 나눠 쓴다. 폼 상태에서는 첫 인풋의 margin 이 「비밀번호」\n * 라벨과의 간격을 만드는데, 이 문장은 margin 이 없어 라벨에 붙어 있었다 — 같은 자리에\n * 두 가지 간격이 생겼다. 값을 새로 정하지 않고 인풋과 같은 토큰을 쓴다.\n */\n margin: var(--change-password-field-margin, var(--spacing-small) 0);\n padding: 12px 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-left: 3px solid var(--md-sys-color-primary);\n border-radius: 7px;\n background: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n\n .changed md-icon {\n color: var(--md-sys-color-primary);\n }\n\n .changed button {\n margin-left: auto;\n border: 1px solid var(--md-sys-color-outline-variant);\n background: transparent;\n color: inherit;\n border-radius: 7px;\n padding: 5px 11px;\n cursor: pointer;\n }\n\n .helper-text {\n font-size: 12px;\n color: var(--md-sys-color-on-surface-variant);\n margin-top: 4px;\n display: block; /* 텍스트를 입력 필드 아래에 배치 */\n line-height: 1.5; /* 텍스트 줄 간격 조절 */\n }\n `\n ]\n\n @property({ type: Object }) passwordRule: {\n lowerCase?: boolean\n upperCase?: boolean\n digit?: boolean\n specialCharacter?: boolean\n allowRepeat?: boolean\n useTightPattern?: boolean\n useLoosePattern?: boolean\n tightCharacterLength?: number\n looseCharacterLength?: number\n } = {}\n\n @query('form') form!: HTMLFormElement\n\n /**\n * Set once the change went through.\n *\n * The server answers with a message and the shell shows it as a toast, but a toast leaves and\n * the same three empty boxes stay behind. A person who has just changed their password reads\n * that as \"nothing happened\" — the report that brought this in said exactly that.\n *\n * So the form is replaced by a statement that stays. It also stops the boxes from inviting a\n * second attempt at something already done.\n */\n @state() private changed = false\n\n private passwordPattern: string = ''\n private passwordHelp: string = ''\n\n render() {\n if (this.changed) {\n return html`\n <div class=\"changed\" role=\"status\">\n <md-icon>check_circle</md-icon>\n <span>${i18next.t('text.password changed successfully')}</span>\n <button type=\"button\" @click=${() => (this.changed = false)}>\n ${i18next.t('text.change password')}\n </button>\n </div>\n `\n }\n\n return html`\n <form>\n <div class=\"field\">\n <input type=\"password\" name=\"current_pass\" placeholder=${i18next.t('text.current password')} required />\n </div>\n <!--\n The rule belongs under the field the rule applies to.\n \n It used to sit between the two, directly beneath the current password, so a sighted\n reader took it as the rule for that field — and the current password has no reason to\n satisfy today's rule. It may predate it. The server only checks that it matches\n (change-pwd.ts verifies the current one and validates the new one against the rule).\n aria-describedby always pointed at the new field, so a screen reader had it right and\n only the eye was misled.\n -->\n <div class=\"field\">\n <input\n type=\"password\"\n name=\"new_pass\"\n placeholder=${i18next.t('text.new password')}\n required\n pattern=${this.passwordPattern}\n aria-describedby=\"password-helper\"\n />\n </div>\n <span id=\"password-helper\" class=\"helper-text\">${this.passwordHelp}</span>\n <div class=\"field\">\n <input type=\"password\" name=\"confirm_pass\" placeholder=${i18next.t('text.confirm password')} required />\n </div>\n\n <!--\n type=\"button\", the way abstract-sign and abstract-auth-page already declare it.\n \n A Material button inside a form defaults to type=\"submit\" and submits unless the click\n is default-prevented. So one click ran submit() and then let the browser submit the\n form as well. submit() had already called form.reset(), so constraint validation found\n three empty required fields, rejected the first one, and put the caret back on the\n current password with an English \"Please fill out this field.\" bubble on a Korean\n screen. It read as the current password being refused.\n [확인함] 2026-09-09 · 사용자 보고 · 화면 사진\n -->\n <md-elevated-button type=\"button\" @click=${this.submit.bind(this)}\n >${i18next.t('text.change password')}</md-elevated-button\n >\n </form>\n `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('passwordRule')) {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n }\n\n languageUpdated() {\n this.passwordPattern = generatePasswordPatternRegExp(this.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.passwordRule)\n }\n\n async submit() {\n const formData = new FormData(this.form)\n let params = {}\n for (const [key, value] of formData.entries()) {\n if (!value) {\n const placeholder = (this.form.querySelector(`[name=${key}]`) as HTMLInputElement)?.placeholder\n return this.showToast(i18next.t('error.value is empty', { value: placeholder || key }))\n }\n params[key] = value\n }\n\n if (params['new_pass'] !== params['confirm_pass']) {\n return this.showToast(i18next.t('error.new-password-and-confirm-password-do-not-match'))\n }\n\n /* Clear only once it took — a refused change used to wipe all three fields. */\n if (await auth.changePassword(params)) {\n this.form.reset()\n this.changed = true\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@material/web/checkbox/checkbox.js';
3
+ import '@operato/input/ox-input-search.js';
4
+ import { LitElement } from 'lit';
5
+ import type { CSSResultGroup } from 'lit';
6
+ /**
7
+ * 「할 수 있는 일 더하기」의 **그릇**.
8
+ *
9
+ * ── 왜 팝업인가 (사용자, 2026-09-11) ────────────────────────────────────────
10
+ * 「'할 수 있는 일 더하기' 작동을 팝업으로 넓게 할 수 있었으면 어떤가? 지금은 좁아보이고」
11
+ *
12
+ * 페이지 안에 펼치면 본문(할 수 있는 일)이 아래로 밀리고, 남은 폭에서 모듈 46개와 축 목록을
13
+ * 함께 봐야 한다. 고르는 동안은 **고르는 것만** 보는 것이 맞다.
14
+ *
15
+ * ── 이 부품은 아무것도 안 그린다 ────────────────────────────────────────────
16
+ * 모듈 접기·축 줄·유령 규칙이 전부 페이지의 상태(`modules`·`assigned`·`selected`·`keyword`)를
17
+ * 본다. 그것을 여기로 옮기면 **상태가 두 벌**이 되고, 그 순간 어느 쪽이 저장 범위인지
18
+ * 흐려진다 — 옛 편집기가 `querySelectorAll` 로 범위를 만들던 결함이 정확히 그 모양이었다.
19
+ *
20
+ * 그래서 그리기는 페이지가 하고(`renderPickerBody`), 여기는 담아서 띄우기만 한다. 대신
21
+ * 페이지가 토글할 때 이 부품에게 다시 그리라고 알린다(`page.picker`).
22
+ *
23
+ * ── 스타일도 한 벌이다 ──────────────────────────────────────────────────────
24
+ * 팝업은 페이지의 shadow root **밖**에 뜨므로 페이지 스타일이 여기까지 안 온다. 베껴 두면
25
+ * 한쪽만 고쳐지는 날이 오고, 오늘 이 화면에서 그 부류를 이미 만났다 — 칩 시절 CSS 가 남아
26
+ * 새 규칙을 이겼다. 그래서 페이지의 `styles` 를 그대로 가져간다.
27
+ *
28
+ * 페이지가 이 파일을 **동적으로** 들이기 때문에 이 import 가 순환이 되지 않는다.
29
+ */
30
+ export declare class RolePrivilegePicker extends LitElement {
31
+ static styles: CSSResultGroup;
32
+ /** 담는 페이지. 이 부품은 그것의 `renderPickerBody()` 를 그린다. */
33
+ page?: any;
34
+ connectedCallback(): void;
35
+ disconnectedCallback(): void;
36
+ render(): any;
37
+ }
@@ -0,0 +1,58 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@material/web/checkbox/checkbox.js';
4
+ import '@operato/input/ox-input-search.js';
5
+ import { html, LitElement } from 'lit';
6
+ import { customElement, property } from 'lit/decorators.js';
7
+ import { RolePrivilegePage } from '../pages/role/role-privilege-page.js';
8
+ /**
9
+ * 「할 수 있는 일 더하기」의 **그릇**.
10
+ *
11
+ * ── 왜 팝업인가 (사용자, 2026-09-11) ────────────────────────────────────────
12
+ * 「'할 수 있는 일 더하기' 작동을 팝업으로 넓게 할 수 있었으면 어떤가? 지금은 좁아보이고」
13
+ *
14
+ * 페이지 안에 펼치면 본문(할 수 있는 일)이 아래로 밀리고, 남은 폭에서 모듈 46개와 축 목록을
15
+ * 함께 봐야 한다. 고르는 동안은 **고르는 것만** 보는 것이 맞다.
16
+ *
17
+ * ── 이 부품은 아무것도 안 그린다 ────────────────────────────────────────────
18
+ * 모듈 접기·축 줄·유령 규칙이 전부 페이지의 상태(`modules`·`assigned`·`selected`·`keyword`)를
19
+ * 본다. 그것을 여기로 옮기면 **상태가 두 벌**이 되고, 그 순간 어느 쪽이 저장 범위인지
20
+ * 흐려진다 — 옛 편집기가 `querySelectorAll` 로 범위를 만들던 결함이 정확히 그 모양이었다.
21
+ *
22
+ * 그래서 그리기는 페이지가 하고(`renderPickerBody`), 여기는 담아서 띄우기만 한다. 대신
23
+ * 페이지가 토글할 때 이 부품에게 다시 그리라고 알린다(`page.picker`).
24
+ *
25
+ * ── 스타일도 한 벌이다 ──────────────────────────────────────────────────────
26
+ * 팝업은 페이지의 shadow root **밖**에 뜨므로 페이지 스타일이 여기까지 안 온다. 베껴 두면
27
+ * 한쪽만 고쳐지는 날이 오고, 오늘 이 화면에서 그 부류를 이미 만났다 — 칩 시절 CSS 가 남아
28
+ * 새 규칙을 이겼다. 그래서 페이지의 `styles` 를 그대로 가져간다.
29
+ *
30
+ * 페이지가 이 파일을 **동적으로** 들이기 때문에 이 import 가 순환이 되지 않는다.
31
+ */
32
+ let RolePrivilegePicker = class RolePrivilegePicker extends LitElement {
33
+ static { this.styles = RolePrivilegePage.styles; }
34
+ connectedCallback() {
35
+ super.connectedCallback();
36
+ if (this.page) {
37
+ this.page.picker = this;
38
+ }
39
+ }
40
+ disconnectedCallback() {
41
+ super.disconnectedCallback();
42
+ if (this.page?.picker === this) {
43
+ this.page.picker = undefined;
44
+ }
45
+ }
46
+ render() {
47
+ return this.page?.renderPickerBody?.() ?? html ``;
48
+ }
49
+ };
50
+ __decorate([
51
+ property({ type: Object }),
52
+ __metadata("design:type", Object)
53
+ ], RolePrivilegePicker.prototype, "page", void 0);
54
+ RolePrivilegePicker = __decorate([
55
+ customElement('role-privilege-picker')
56
+ ], RolePrivilegePicker);
57
+ export { RolePrivilegePicker };
58
+ //# sourceMappingURL=role-privilege-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-privilege-picker.js","sourceRoot":"","sources":["../../client/components/role-privilege-picker.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,oCAAoC,CAAA;AAC3C,OAAO,mCAAmC,CAAA;AAE1C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAEtC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAExE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,UAAU;aAC1C,WAAM,GAAmB,iBAAiB,CAAC,MAAwB,AAA7D,CAA6D;IAK1E,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QACzB,CAAC;IACH,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QAC9B,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,EAAE,IAAI,IAAI,CAAA,EAAE,CAAA;IAClD,CAAC;;AAlB2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;iDAAW;AAJ3B,mBAAmB;IAD/B,aAAa,CAAC,uBAAuB,CAAC;GAC1B,mBAAmB,CAuB/B","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/checkbox/checkbox.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { html, LitElement } from 'lit'\nimport type { CSSResultGroup } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { RolePrivilegePage } from '../pages/role/role-privilege-page.js'\n\n/**\n * 「할 수 있는 일 더하기」의 **그릇**.\n *\n * ── 왜 팝업인가 (사용자, 2026-09-11) ────────────────────────────────────────\n * 「'할 수 있는 일 더하기' 작동을 팝업으로 넓게 할 수 있었으면 어떤가? 지금은 좁아보이고」\n *\n * 페이지 안에 펼치면 본문(할 수 있는 일)이 아래로 밀리고, 남은 폭에서 모듈 46개와 축 목록을\n * 함께 봐야 한다. 고르는 동안은 **고르는 것만** 보는 것이 맞다.\n *\n * ── 이 부품은 아무것도 안 그린다 ────────────────────────────────────────────\n * 모듈 접기·축 줄·유령 규칙이 전부 페이지의 상태(`modules`·`assigned`·`selected`·`keyword`)를\n * 본다. 그것을 여기로 옮기면 **상태가 두 벌**이 되고, 그 순간 어느 쪽이 저장 범위인지\n * 흐려진다 — 옛 편집기가 `querySelectorAll` 로 범위를 만들던 결함이 정확히 그 모양이었다.\n *\n * 그래서 그리기는 페이지가 하고(`renderPickerBody`), 여기는 담아서 띄우기만 한다. 대신\n * 페이지가 토글할 때 이 부품에게 다시 그리라고 알린다(`page.picker`).\n *\n * ── 스타일도 한 벌이다 ──────────────────────────────────────────────────────\n * 팝업은 페이지의 shadow root **밖**에 뜨므로 페이지 스타일이 여기까지 안 온다. 베껴 두면\n * 한쪽만 고쳐지는 날이 오고, 오늘 이 화면에서 그 부류를 이미 만났다 — 칩 시절 CSS 가 남아\n * 새 규칙을 이겼다. 그래서 페이지의 `styles` 를 그대로 가져간다.\n *\n * 페이지가 이 파일을 **동적으로** 들이기 때문에 이 import 가 순환이 되지 않는다.\n */\n@customElement('role-privilege-picker')\nexport class RolePrivilegePicker extends LitElement {\n static styles: CSSResultGroup = RolePrivilegePage.styles as CSSResultGroup\n\n /** 담는 페이지. 이 부품은 그것의 `renderPickerBody()` 를 그린다. */\n @property({ type: Object }) page?: any\n\n connectedCallback() {\n super.connectedCallback()\n if (this.page) {\n this.page.picker = this\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n if (this.page?.picker === this) {\n this.page.picker = undefined\n }\n }\n\n render() {\n return this.page?.renderPickerBody?.() ?? html``\n }\n}\n"]}
@@ -0,0 +1,83 @@
1
+ import '@material/web/icon/icon.js';
2
+ import { LitElement } from 'lit';
3
+ import type { CSSResultGroup } from 'lit';
4
+ declare const RoleTemplatePicker_base: (new (...args: any[]) => LitElement) & typeof LitElement;
5
+ /**
6
+ * Starting a role.
7
+ *
8
+ * ── Why this stands where the blank row used to ──────────────────────────────
9
+ * A new plant has no roles, and the old answer to "make one" was an empty line waiting for
10
+ * a name. Naming it is the easy half; the hard half is which of 84 privileges belong to a
11
+ * quality inspector, and that is a judgement the product has made and the plant has not.
12
+ *
13
+ * miratek is the case: two roles (Administrator, TWIN-CONNECTOR), no MES seats, and eleven
14
+ * approval doors standing empty because there is nothing to route them to. Read from
15
+ * inside, that looks like a plant that has not decided. It is a plant with nothing to
16
+ * decide from.
17
+ *
18
+ * ── Both doors, in one place ─────────────────────────────────────────────────
19
+ * The templates come first and the blank name last, because the order is what teaches: a
20
+ * plant that sees "quality inspector, 7 privileges" learns what a seat looks like before
21
+ * it is asked to invent one. Hiding the blank door would be worse, though — the seats a
22
+ * factory actually has ("night shift lead", "outside inspector") are its own, and the
23
+ * product does not know them.
24
+ *
25
+ * ── Why the name is editable ─────────────────────────────────────────────────
26
+ * The template knows the seat exists; it does not know what the people there call it. And
27
+ * because a role is also what an approval line points at, the same seat is sometimes
28
+ * needed twice — a day and a night inspector hold the same privileges and take different
29
+ * approvals. A fixed name would allow each template exactly once per domain.
30
+ */
31
+ export declare class RoleTemplatePicker extends RoleTemplatePicker_base {
32
+ static styles: CSSResultGroup;
33
+ page?: any;
34
+ private templates;
35
+ private selected?;
36
+ /** `undefined` means "the blank door" — name the seat yourself. */
37
+ private scratch;
38
+ private name;
39
+ private description;
40
+ connectedCallback(): Promise<void>;
41
+ private fetchTemplates;
42
+ render(): import("lit-html").TemplateResult<1>;
43
+ /**
44
+ * What the seat opens, in one line — **counted, not interpreted.**
45
+ *
46
+ * This briefly said "changes 6 · only sees 8", which read well and was not something the
47
+ * data supports: the privilege axis is an unconstrained string and its name does not
48
+ * determine what the resolver behind it does. Counting is safe; grouping by meaning was
49
+ * not. See `renderOpens`.
50
+ *
51
+ * "0 privileges" is still two different facts and must not be merged: granting nothing
52
+ * on purpose is an approval seat, and a template whose grants are all missing here is a
53
+ * defect (`missingCount`).
54
+ */
55
+ private summarize;
56
+ private renderDetail;
57
+ /**
58
+ * 이 자리가 무엇을 여나 — **선언된 것만.**
59
+ *
60
+ * ⚠ 여기 「바꿀 수 있는 것 / 볼 수만 있는 것」 두 묶음이 있었다. 사용자가 물었다(2026-09-11):
61
+ * 「바꿀 수 있고, 볼 수만 있는 것을 너가 어떻게 알아? 논리적이지 않은 것을 우겨넣지 말고.」
62
+ *
63
+ * 몰랐다. 지어낸 것이다. 근거가 없는 이유가 둘이다.
64
+ *
65
+ * ① `@privilege(privilege:)` 는 **제약 없는 문자열**이다(지시자 선언이 `privilege: String`
66
+ * 이고 enum 이 아니다). 지금 쓰인 값이 mutation 484 · query 314 로 둘뿐인 것은 **오늘
67
+ * 그렇게 쓴 것**이지 둘로 한정된 것이 아니다. 내 코드는 `axis !== 'query'` 로 갈랐으므로,
68
+ * 처음 보는 값은 **알아서가 아니라 query 가 아니라서** 「바꿀 수 있는 것」으로 떨어졌다.
69
+ * ② 이름이 뜻을 보장하지 않는다. 축 이름과 실제 GraphQL 연산이 어긋나는 자리가 6곳이고,
70
+ * 그중 둘은 이 기능의 내 파일이다(`role-template-query.ts` — 템플릿을 **읽는** 질의에
71
+ * `privilege: "mutation"` 을 달았다. 역할을 만드는 문이라서 그게 맞다). 축은 연산 종류가
72
+ * 아니라 **어느 문을 지나야 하나**의 이름이다.
73
+ *
74
+ * 그래서 화면은 선언된 것을 그대로 낸다 — 어느 모듈에, 어느 권한이. 읽히게 만드는 것
75
+ * (굵기·줄 간격·가운뎃점)은 아무것도 주장하지 않으므로 그대로 둔다.
76
+ */
77
+ private renderOpens;
78
+ private pick;
79
+ private pickScratch;
80
+ private canCreate;
81
+ private create;
82
+ }
83
+ export {};