@draftlab/auth 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/base.js +228 -228
- package/dist/ui/code.js +20 -88
- package/dist/ui/form.d.ts +0 -1
- package/dist/ui/form.js +35 -45
- package/dist/ui/passkey.d.ts +1 -6
- package/dist/ui/passkey.js +25 -69
- package/dist/ui/password.js +1 -1
- package/package.json +1 -1
package/dist/ui/base.js
CHANGED
|
@@ -6,293 +6,293 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
6
6
|
const css = `@import url("https://unpkg.com/tailwindcss@3.4.15/src/css/preflight.css");
|
|
7
7
|
|
|
8
8
|
:root {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
--font-family:
|
|
69
|
-
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
70
|
-
"Segoe UI Symbol", "Noto Color Emoji";
|
|
71
|
-
--font-scale: 1;
|
|
72
|
-
|
|
73
|
-
--font-size-xs: calc(0.75rem * var(--font-scale));
|
|
74
|
-
--font-size-sm: calc(0.875rem * var(--font-scale));
|
|
75
|
-
--font-size-md: calc(1rem * var(--font-scale));
|
|
76
|
-
--font-size-lg: calc(1.125rem * var(--font-scale));
|
|
77
|
-
--font-size-xl: calc(1.25rem * var(--font-scale));
|
|
78
|
-
--font-size-2xl: calc(1.5rem * var(--font-scale));
|
|
9
|
+
--color-background-dark: #0e0e11;
|
|
10
|
+
--color-background-light: #ffffff;
|
|
11
|
+
--color-primary-dark: #6772e5;
|
|
12
|
+
--color-primary-light: #6772e5;
|
|
13
|
+
|
|
14
|
+
--color-background-success-dark: oklch(0.3 0.04 172);
|
|
15
|
+
--color-background-success-light: oklch(from var(--color-background-success-dark) 0.83 c h);
|
|
16
|
+
--color-success-dark: oklch(from var(--color-background-success-dark) 0.92 c h);
|
|
17
|
+
--color-success-light: oklch(from var(--color-background-success-dark) 0.25 c h);
|
|
18
|
+
|
|
19
|
+
--color-background-error-dark: oklch(0.32 0.07 15);
|
|
20
|
+
--color-background-error-light: oklch(from var(--color-background-error-dark) 0.92 c h);
|
|
21
|
+
--color-error-dark: oklch(from var(--color-background-error-dark) 0.92 c h);
|
|
22
|
+
--color-error-light: oklch(from var(--color-background-error-dark) 0.25 c h);
|
|
23
|
+
|
|
24
|
+
--border-radius: 0;
|
|
25
|
+
|
|
26
|
+
--color-background: var(--color-background-dark);
|
|
27
|
+
--color-primary: var(--color-primary-dark);
|
|
28
|
+
|
|
29
|
+
--color-background-success: var(--color-background-success-dark);
|
|
30
|
+
--color-success: var(--color-success-dark);
|
|
31
|
+
--color-background-error: var(--color-background-error-dark);
|
|
32
|
+
--color-error: var(--color-error-dark);
|
|
33
|
+
|
|
34
|
+
@media (prefers-color-scheme: light) {
|
|
35
|
+
--color-background: var(--color-background-light);
|
|
36
|
+
--color-primary: var(--color-primary-light);
|
|
37
|
+
|
|
38
|
+
--color-background-success: var(--color-background-success-light);
|
|
39
|
+
--color-success: var(--color-success-light);
|
|
40
|
+
--color-background-error: var(--color-background-error-light);
|
|
41
|
+
--color-error: var(--color-error-light);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
--color-high: oklch(
|
|
45
|
+
from var(--color-background) clamp(0, calc((l - 0.714) * -1000), 1) 0 0
|
|
46
|
+
);
|
|
47
|
+
--color-low: oklch(from var(--color-background) clamp(0, calc((l - 0.714) * 1000), 1) 0 0);
|
|
48
|
+
--lightness-high: color-mix(
|
|
49
|
+
in oklch,
|
|
50
|
+
var(--color-high) 0%,
|
|
51
|
+
oklch(var(--color-high) 0 0)
|
|
52
|
+
);
|
|
53
|
+
--lightness-low: color-mix(
|
|
54
|
+
in oklch,
|
|
55
|
+
var(--color-low) 0%,
|
|
56
|
+
oklch(var(--color-low) 0 0)
|
|
57
|
+
);
|
|
58
|
+
--font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
|
59
|
+
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
60
|
+
--font-scale: 1;
|
|
61
|
+
|
|
62
|
+
--font-size-xs: calc(0.75rem * var(--font-scale));
|
|
63
|
+
--font-size-sm: calc(0.875rem * var(--font-scale));
|
|
64
|
+
--font-size-md: calc(1rem * var(--font-scale));
|
|
65
|
+
--font-size-lg: calc(1.125rem * var(--font-scale));
|
|
66
|
+
--font-size-xl: calc(1.25rem * var(--font-scale));
|
|
67
|
+
--font-size-2xl: calc(1.5rem * var(--font-scale));
|
|
79
68
|
}
|
|
80
69
|
|
|
81
70
|
[data-component="root"] {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
71
|
+
font-family: var(--font-family);
|
|
72
|
+
background-color: var(--color-background);
|
|
73
|
+
padding: 1rem;
|
|
74
|
+
color: white;
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
user-select: none;
|
|
82
|
+
color: var(--color-high);
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
[data-component="center"] {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
86
|
+
width: 380px;
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: 1.5rem;
|
|
101
90
|
}
|
|
102
91
|
|
|
103
92
|
[data-component="center"][data-size="small"] {
|
|
104
|
-
|
|
93
|
+
width: 300px;
|
|
105
94
|
}
|
|
106
95
|
|
|
107
|
-
[data-component="
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
font-weight: 500;
|
|
112
|
-
font-size: var(--font-size-sm);
|
|
113
|
-
border-radius: calc(var(--border-radius) * 0.25rem);
|
|
114
|
-
display: flex;
|
|
115
|
-
gap: 0.75rem;
|
|
116
|
-
align-items: center;
|
|
117
|
-
justify-content: center;
|
|
118
|
-
background: var(--color-primary);
|
|
119
|
-
color: oklch(
|
|
120
|
-
from var(--color-primary) clamp(0, calc((l - 0.714) * -1000), 1) 0 0
|
|
121
|
-
);
|
|
96
|
+
[data-component="link"] {
|
|
97
|
+
text-decoration: underline;
|
|
98
|
+
text-underline-offset: 0.125rem;
|
|
99
|
+
font-weight: 600;
|
|
122
100
|
}
|
|
123
101
|
|
|
124
|
-
[data-component="
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
from var(--color-background)
|
|
130
|
-
calc(
|
|
131
|
-
clamp(
|
|
132
|
-
0.22,
|
|
133
|
-
l +
|
|
134
|
-
(-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06),
|
|
135
|
-
0.88
|
|
136
|
-
)
|
|
137
|
-
)
|
|
138
|
-
c h
|
|
139
|
-
);
|
|
102
|
+
[data-component="label"] {
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 0.75rem;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
font-size: var(--font-size-xs);
|
|
140
107
|
}
|
|
141
108
|
|
|
142
|
-
[data-component="
|
|
143
|
-
|
|
144
|
-
|
|
109
|
+
[data-component="logo"] {
|
|
110
|
+
margin: 0 auto;
|
|
111
|
+
height: 2.5rem;
|
|
112
|
+
width: auto;
|
|
113
|
+
display: none;
|
|
145
114
|
}
|
|
146
115
|
|
|
147
|
-
[data-component="
|
|
148
|
-
|
|
149
|
-
height: 100%;
|
|
116
|
+
[data-component="logo"][data-mode="light"] {
|
|
117
|
+
display: none;
|
|
150
118
|
}
|
|
151
119
|
|
|
152
|
-
[data-component="
|
|
153
|
-
|
|
154
|
-
display: flex;
|
|
155
|
-
flex-direction: column;
|
|
156
|
-
gap: 1rem;
|
|
157
|
-
margin: 0;
|
|
120
|
+
[data-component="logo"][data-mode="dark"] {
|
|
121
|
+
display: block;
|
|
158
122
|
}
|
|
159
123
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
l +
|
|
169
|
-
(-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06),
|
|
170
|
-
0.88
|
|
171
|
-
)
|
|
172
|
-
)
|
|
173
|
-
c h
|
|
174
|
-
);
|
|
175
|
-
border-radius: calc(var(--border-radius) * 0.25rem);
|
|
176
|
-
background: var(--color-background);
|
|
177
|
-
color: var(--color-high);
|
|
178
|
-
padding: 0 0.75rem;
|
|
179
|
-
font-size: var(--font-size-sm);
|
|
180
|
-
font-family: var(--font-family);
|
|
124
|
+
@media (prefers-color-scheme: light) {
|
|
125
|
+
[data-component="logo"][data-mode="light"] {
|
|
126
|
+
display: block;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[data-component="logo"][data-mode="dark"] {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
181
132
|
}
|
|
182
133
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
134
|
+
@media (prefers-color-scheme: dark) {
|
|
135
|
+
[data-component="logo"][data-mode="dark"] {
|
|
136
|
+
display: block;
|
|
137
|
+
}
|
|
186
138
|
}
|
|
187
139
|
|
|
188
|
-
[data-component="
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
c
|
|
193
|
-
h /
|
|
194
|
-
0.6
|
|
195
|
-
);
|
|
140
|
+
[data-component="logo-default"] {
|
|
141
|
+
margin: 0 auto;
|
|
142
|
+
height: 2.5rem;
|
|
143
|
+
width: auto;
|
|
196
144
|
}
|
|
197
145
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
align-items: center;
|
|
203
|
-
gap: 0.75rem;
|
|
204
|
-
font-size: var(--font-size-sm);
|
|
146
|
+
@media (prefers-color-scheme: light) {
|
|
147
|
+
[data-component="logo-default"] {
|
|
148
|
+
color: var(--color-high);
|
|
149
|
+
}
|
|
205
150
|
}
|
|
206
151
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
152
|
+
@media (prefers-color-scheme: dark) {
|
|
153
|
+
[data-component="logo-default"] {
|
|
154
|
+
color: var(--color-high);
|
|
155
|
+
}
|
|
211
156
|
}
|
|
212
157
|
|
|
213
|
-
[data-component="
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
158
|
+
[data-component="input"] {
|
|
159
|
+
width: 100%;
|
|
160
|
+
height: 2.5rem;
|
|
161
|
+
padding: 0 1rem;
|
|
162
|
+
border: 1px solid transparent;
|
|
163
|
+
--background: oklch(
|
|
164
|
+
from var(--color-background) calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h
|
|
165
|
+
);
|
|
166
|
+
background: var(--background);
|
|
167
|
+
border-color: oklch(
|
|
168
|
+
from var(--color-background)
|
|
169
|
+
calc(clamp(0.22, l + (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06), 0.88)) c h
|
|
170
|
+
);
|
|
171
|
+
border-radius: calc(var(--border-radius) * 0.25rem);
|
|
172
|
+
font-size: var(--font-size-sm);
|
|
173
|
+
outline: none;
|
|
217
174
|
}
|
|
218
175
|
|
|
219
|
-
[data-component="
|
|
220
|
-
|
|
176
|
+
[data-component="input"]:focus {
|
|
177
|
+
border-color: oklch(
|
|
178
|
+
from var(--color-background)
|
|
179
|
+
calc(clamp(0.3, l + (-0.2 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.1), 0.7)) c h
|
|
180
|
+
);
|
|
221
181
|
}
|
|
222
182
|
|
|
223
|
-
[data-component="
|
|
224
|
-
|
|
183
|
+
[data-component="input"]:user-invalid:not(:focus) {
|
|
184
|
+
border-color: oklch(0.4 0.09 7.91);
|
|
225
185
|
}
|
|
226
186
|
|
|
227
|
-
[data-component="
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
187
|
+
[data-component="button"] {
|
|
188
|
+
height: 2.5rem;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
border: 0;
|
|
191
|
+
font-weight: 500;
|
|
192
|
+
font-size: var(--font-size-sm);
|
|
193
|
+
border-radius: calc(var(--border-radius) * 0.25rem);
|
|
194
|
+
display: flex;
|
|
195
|
+
gap: 0.75rem;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
background: var(--color-primary);
|
|
199
|
+
color: oklch(from var(--color-primary) clamp(0, calc((l - 0.714) * -1000), 1) 0 0);
|
|
231
200
|
}
|
|
232
201
|
|
|
233
|
-
[data-component="
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
202
|
+
[data-component="button"][data-color="ghost"] {
|
|
203
|
+
background: transparent;
|
|
204
|
+
color: var(--color-high);
|
|
205
|
+
border: 1px solid
|
|
206
|
+
oklch(
|
|
207
|
+
from var(--color-background)
|
|
208
|
+
calc(clamp(0.22, l + (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06), 0.88)) c h
|
|
209
|
+
);
|
|
237
210
|
}
|
|
238
211
|
|
|
239
|
-
[data-component="
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
font-size: var(--font-size-sm);
|
|
212
|
+
[data-component="button"] [data-slot="icon"] {
|
|
213
|
+
width: 16px;
|
|
214
|
+
height: 16px;
|
|
243
215
|
}
|
|
244
216
|
|
|
245
|
-
[data-component="
|
|
246
|
-
|
|
217
|
+
[data-component="button"] [data-slot="icon"] svg {
|
|
218
|
+
width: 100%;
|
|
219
|
+
height: 100%;
|
|
247
220
|
}
|
|
248
221
|
|
|
249
|
-
[data-component="form
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
222
|
+
[data-component="form"] {
|
|
223
|
+
max-width: 100%;
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 1rem;
|
|
227
|
+
margin: 0;
|
|
255
228
|
}
|
|
256
229
|
|
|
257
|
-
[data-component="form-
|
|
258
|
-
|
|
230
|
+
[data-component="form-alert"] {
|
|
231
|
+
height: 2.5rem;
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
padding: 0 1rem;
|
|
235
|
+
border-radius: calc(var(--border-radius) * 0.25rem);
|
|
236
|
+
background: var(--color-background-error);
|
|
237
|
+
color: var(--color-error);
|
|
238
|
+
text-align: left;
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
gap: 0.5rem;
|
|
259
241
|
}
|
|
260
242
|
|
|
261
|
-
[data-component="
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
width: auto;
|
|
243
|
+
[data-component="form-alert"][data-color="success"] {
|
|
244
|
+
background: var(--color-background-success);
|
|
245
|
+
color: var(--color-success);
|
|
265
246
|
}
|
|
266
247
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
color: var(--color-high);
|
|
270
|
-
}
|
|
248
|
+
[data-component="form-alert"][data-color="success"] [data-slot="icon-success"] {
|
|
249
|
+
display: block;
|
|
271
250
|
}
|
|
272
251
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
color: var(--color-high);
|
|
276
|
-
}
|
|
252
|
+
[data-component="form-alert"][data-color="success"] [data-slot="icon-danger"] {
|
|
253
|
+
display: none;
|
|
277
254
|
}
|
|
278
255
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
display: none;
|
|
256
|
+
[data-component="form-alert"]:has([data-slot="message"]:empty) {
|
|
257
|
+
display: none;
|
|
282
258
|
}
|
|
283
259
|
|
|
284
|
-
[data-component="
|
|
285
|
-
|
|
260
|
+
[data-component="form-alert"] [data-slot="icon-success"],
|
|
261
|
+
[data-component="form-alert"] [data-slot="icon-danger"] {
|
|
262
|
+
width: 1rem;
|
|
263
|
+
height: 1rem;
|
|
286
264
|
}
|
|
287
265
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
266
|
+
[data-component="form-alert"] [data-slot="icon-success"] {
|
|
267
|
+
display: none;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
[data-component="form-footer"] {
|
|
271
|
+
display: flex;
|
|
272
|
+
gap: 1rem;
|
|
273
|
+
font-size: 0.75rem;
|
|
274
|
+
align-items: center;
|
|
275
|
+
justify-content: center;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
[data-component="form-footer"]:has(> :nth-child(2)) {
|
|
279
|
+
justify-content: space-between;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
[data-component="title"] {
|
|
283
|
+
font-size: var(--font-size-2xl);
|
|
284
|
+
font-weight: 600;
|
|
285
|
+
margin: 0 0 0.5rem 0;
|
|
286
|
+
color: var(--color-high);
|
|
287
|
+
text-align: center;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
[data-component="description"] {
|
|
291
|
+
font-size: var(--font-size-sm);
|
|
292
|
+
color: var(--color-high);
|
|
293
|
+
margin: 0 0 1.5rem 0;
|
|
294
|
+
text-align: center;
|
|
295
|
+
opacity: 0.8;
|
|
296
296
|
}
|
|
297
297
|
`;
|
|
298
298
|
/**
|
package/dist/ui/code.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Layout, renderToHTML } from "./base.js";
|
|
2
|
+
import { FormAlert } from "./form.js";
|
|
2
3
|
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
3
4
|
|
|
4
5
|
//#region src/ui/code.tsx
|
|
@@ -18,74 +19,6 @@ const DEFAULT_COPY = {
|
|
|
18
19
|
code_resend: "Resend"
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
|
-
* FormAlert component for displaying messages
|
|
22
|
-
*/
|
|
23
|
-
const FormAlert = ({ message, color = "danger" }) => {
|
|
24
|
-
if (!message) return null;
|
|
25
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
26
|
-
"data-component": "form-alert",
|
|
27
|
-
"data-color": color,
|
|
28
|
-
children: [/* @__PURE__ */ jsx("i", {
|
|
29
|
-
"data-slot": color === "success" ? "icon-success" : "icon-danger",
|
|
30
|
-
children: color === "success" ? /* @__PURE__ */ jsx("svg", {
|
|
31
|
-
fill: "none",
|
|
32
|
-
stroke: "currentColor",
|
|
33
|
-
viewBox: "0 0 24 24",
|
|
34
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
35
|
-
"aria-label": "Success",
|
|
36
|
-
role: "img",
|
|
37
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
38
|
-
strokeLinecap: "round",
|
|
39
|
-
strokeLinejoin: "round",
|
|
40
|
-
strokeWidth: 2,
|
|
41
|
-
d: "M5 13l4 4L19 7"
|
|
42
|
-
})
|
|
43
|
-
}) : /* @__PURE__ */ jsx("svg", {
|
|
44
|
-
fill: "none",
|
|
45
|
-
stroke: "currentColor",
|
|
46
|
-
viewBox: "0 0 24 24",
|
|
47
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
48
|
-
"aria-label": "Error",
|
|
49
|
-
role: "img",
|
|
50
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
51
|
-
strokeLinecap: "round",
|
|
52
|
-
strokeLinejoin: "round",
|
|
53
|
-
strokeWidth: 2,
|
|
54
|
-
d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.232 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
58
|
-
"data-slot": "message",
|
|
59
|
-
children: message
|
|
60
|
-
})]
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Input component with consistent styling
|
|
65
|
-
*/
|
|
66
|
-
const Input = ({ type, name, placeholder, value, required, autoComplete, autoFocus,...props }) => /* @__PURE__ */ jsx("input", {
|
|
67
|
-
type,
|
|
68
|
-
name,
|
|
69
|
-
placeholder,
|
|
70
|
-
value,
|
|
71
|
-
required,
|
|
72
|
-
autoComplete,
|
|
73
|
-
"data-component": "input",
|
|
74
|
-
...props
|
|
75
|
-
});
|
|
76
|
-
/**
|
|
77
|
-
* Button component with consistent styling
|
|
78
|
-
*/
|
|
79
|
-
const Button = ({ type = "submit", children,...props }) => /* @__PURE__ */ jsx("button", {
|
|
80
|
-
type,
|
|
81
|
-
"data-component": "button",
|
|
82
|
-
...props,
|
|
83
|
-
children
|
|
84
|
-
});
|
|
85
|
-
/**
|
|
86
|
-
* Link component with consistent styling
|
|
87
|
-
*/
|
|
88
|
-
/**
|
|
89
22
|
* Gets the appropriate error message for display
|
|
90
23
|
*/
|
|
91
24
|
const getErrorMessage = (error, copy) => {
|
|
@@ -129,7 +62,8 @@ const CodeUI = (options) => {
|
|
|
129
62
|
message: success.message,
|
|
130
63
|
color: "success"
|
|
131
64
|
}) : /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error, copy) }),
|
|
132
|
-
/* @__PURE__ */ jsx(
|
|
65
|
+
/* @__PURE__ */ jsx("input", {
|
|
66
|
+
"data-component": "input",
|
|
133
67
|
type: mode === "email" ? "email" : "tel",
|
|
134
68
|
name: mode,
|
|
135
69
|
placeholder: copy.email_placeholder,
|
|
@@ -142,17 +76,13 @@ const CodeUI = (options) => {
|
|
|
142
76
|
name: "action",
|
|
143
77
|
value: "request"
|
|
144
78
|
}),
|
|
145
|
-
/* @__PURE__ */ jsx(
|
|
79
|
+
/* @__PURE__ */ jsx("button", {
|
|
80
|
+
"data-component": "button",
|
|
146
81
|
type: "submit",
|
|
147
82
|
children: copy.button_continue
|
|
148
83
|
}),
|
|
149
84
|
/* @__PURE__ */ jsx("p", {
|
|
150
|
-
|
|
151
|
-
fontSize: "0.875rem",
|
|
152
|
-
color: "var(--color-high)",
|
|
153
|
-
textAlign: "center",
|
|
154
|
-
margin: "1rem 0 0 0"
|
|
155
|
-
},
|
|
85
|
+
"data-component": "description",
|
|
156
86
|
children: copy.code_info
|
|
157
87
|
})
|
|
158
88
|
]
|
|
@@ -177,7 +107,8 @@ const CodeUI = (options) => {
|
|
|
177
107
|
type: "hidden",
|
|
178
108
|
value: "verify"
|
|
179
109
|
}),
|
|
180
|
-
/* @__PURE__ */ jsx(
|
|
110
|
+
/* @__PURE__ */ jsx("input", {
|
|
111
|
+
"data-component": "input",
|
|
181
112
|
type: "text",
|
|
182
113
|
name: "code",
|
|
183
114
|
placeholder: copy.code_placeholder,
|
|
@@ -187,10 +118,10 @@ const CodeUI = (options) => {
|
|
|
187
118
|
maxLength: 6,
|
|
188
119
|
minLength: 6,
|
|
189
120
|
pattern: "[0-9]{6}",
|
|
190
|
-
autoFocus: true,
|
|
191
121
|
required: true
|
|
192
122
|
}),
|
|
193
|
-
/* @__PURE__ */ jsx(
|
|
123
|
+
/* @__PURE__ */ jsx("button", {
|
|
124
|
+
"data-component": "button",
|
|
194
125
|
type: "submit",
|
|
195
126
|
children: copy.button_continue
|
|
196
127
|
})
|
|
@@ -208,16 +139,17 @@ const CodeUI = (options) => {
|
|
|
208
139
|
type: "hidden",
|
|
209
140
|
value: contact
|
|
210
141
|
}),
|
|
211
|
-
/* @__PURE__ */
|
|
142
|
+
/* @__PURE__ */ jsx("div", {
|
|
212
143
|
"data-component": "form-footer",
|
|
213
|
-
children:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
144
|
+
children: /* @__PURE__ */ jsxs("span", { children: [
|
|
145
|
+
copy.code_didnt_get,
|
|
146
|
+
" ",
|
|
147
|
+
/* @__PURE__ */ jsx("button", {
|
|
148
|
+
type: "submit",
|
|
149
|
+
"data-component": "link",
|
|
150
|
+
children: copy.code_resend
|
|
151
|
+
})
|
|
152
|
+
] })
|
|
221
153
|
})
|
|
222
154
|
]
|
|
223
155
|
})] });
|
package/dist/ui/form.d.ts
CHANGED
package/dist/ui/form.js
CHANGED
|
@@ -2,56 +2,46 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
2
2
|
|
|
3
3
|
//#region src/ui/form.tsx
|
|
4
4
|
/**
|
|
5
|
-
* Success icon component showing a checkmark in a circle.
|
|
6
|
-
* Used for positive feedback messages.
|
|
7
|
-
*/
|
|
8
|
-
const SuccessIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
9
|
-
"aria-hidden": "true",
|
|
10
|
-
"data-slot": "icon-success",
|
|
11
|
-
fill: "none",
|
|
12
|
-
stroke: "currentColor",
|
|
13
|
-
strokeWidth: "1.5",
|
|
14
|
-
viewBox: "0 0 24 24",
|
|
15
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
16
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
17
|
-
d: "M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
|
|
18
|
-
strokeLinecap: "round",
|
|
19
|
-
strokeLinejoin: "round"
|
|
20
|
-
})
|
|
21
|
-
});
|
|
22
|
-
/**
|
|
23
|
-
* Danger icon component showing an exclamation mark in a circle.
|
|
24
|
-
* Used for error and warning messages.
|
|
25
|
-
*/
|
|
26
|
-
const DangerIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
27
|
-
"aria-hidden": "true",
|
|
28
|
-
"data-slot": "icon-danger",
|
|
29
|
-
fill: "none",
|
|
30
|
-
stroke: "currentColor",
|
|
31
|
-
strokeWidth: "1.5",
|
|
32
|
-
viewBox: "0 0 24 24",
|
|
33
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
34
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
35
|
-
d: "M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z",
|
|
36
|
-
strokeLinecap: "round",
|
|
37
|
-
strokeLinejoin: "round"
|
|
38
|
-
})
|
|
39
|
-
});
|
|
40
|
-
/**
|
|
41
5
|
* Form alert component that displays error or success messages.
|
|
42
|
-
* Returns a Preact component or null if no message.
|
|
43
6
|
*/
|
|
44
7
|
const FormAlert = ({ message, color = "danger" }) => {
|
|
45
|
-
if (!message) return null;
|
|
46
8
|
return /* @__PURE__ */ jsxs("div", {
|
|
47
|
-
"aria-live": "polite",
|
|
48
|
-
"data-color": color,
|
|
49
9
|
"data-component": "form-alert",
|
|
50
|
-
|
|
51
|
-
children: [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
10
|
+
"data-color": color,
|
|
11
|
+
children: [
|
|
12
|
+
/* @__PURE__ */ jsx("svg", {
|
|
13
|
+
"aria-hidden": "true",
|
|
14
|
+
"data-slot": "icon-success",
|
|
15
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
16
|
+
fill: "none",
|
|
17
|
+
viewBox: "0 0 24 24",
|
|
18
|
+
"stroke-width": "1.5",
|
|
19
|
+
stroke: "currentColor",
|
|
20
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
21
|
+
"stroke-linecap": "round",
|
|
22
|
+
"stroke-linejoin": "round",
|
|
23
|
+
d: "M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
|
24
|
+
})
|
|
25
|
+
}),
|
|
26
|
+
/* @__PURE__ */ jsx("svg", {
|
|
27
|
+
"aria-hidden": "true",
|
|
28
|
+
"data-slot": "icon-danger",
|
|
29
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
30
|
+
fill: "none",
|
|
31
|
+
viewBox: "0 0 24 24",
|
|
32
|
+
"stroke-width": "1.5",
|
|
33
|
+
stroke: "currentColor",
|
|
34
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
35
|
+
"stroke-linecap": "round",
|
|
36
|
+
"stroke-linejoin": "round",
|
|
37
|
+
d: "M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
|
|
38
|
+
})
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsx("span", {
|
|
41
|
+
"data-slot": "message",
|
|
42
|
+
children: message
|
|
43
|
+
})
|
|
44
|
+
]
|
|
55
45
|
});
|
|
56
46
|
};
|
|
57
47
|
|
package/dist/ui/passkey.d.ts
CHANGED
|
@@ -6,17 +6,12 @@ import { PasskeyProviderConfig } from "../provider/passkey.js";
|
|
|
6
6
|
* Strongly typed copy text configuration for passkey UI
|
|
7
7
|
*/
|
|
8
8
|
interface PasskeyUICopy {
|
|
9
|
-
readonly authorize_title: string;
|
|
10
|
-
readonly authorize_description: string;
|
|
11
|
-
readonly register_title: string;
|
|
12
|
-
readonly register_description: string;
|
|
13
9
|
readonly register: string;
|
|
14
|
-
readonly
|
|
10
|
+
readonly button_continue: string;
|
|
15
11
|
readonly register_other_device: string;
|
|
16
12
|
readonly register_prompt: string;
|
|
17
13
|
readonly login_prompt: string;
|
|
18
14
|
readonly login: string;
|
|
19
|
-
readonly login_with_passkey: string;
|
|
20
15
|
readonly change_prompt: string;
|
|
21
16
|
readonly code_resend: string;
|
|
22
17
|
readonly code_return: string;
|
package/dist/ui/passkey.js
CHANGED
|
@@ -4,17 +4,12 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
4
4
|
|
|
5
5
|
//#region src/ui/passkey.tsx
|
|
6
6
|
const DEFAULT_COPY = {
|
|
7
|
-
authorize_title: "Sign in with Passkey",
|
|
8
|
-
authorize_description: "Passkeys are a simple and more secure alternative to passwords. With passkeys, you can log in with your PIN, biometric sensor, or hardware security key.",
|
|
9
|
-
register_title: "Create a Passkey",
|
|
10
|
-
register_description: "Create a passkey to enable secure, passwordless authentication for your account.",
|
|
11
7
|
register: "Register",
|
|
12
|
-
register_with_passkey: "Register With Passkey",
|
|
13
8
|
register_other_device: "Use another device",
|
|
14
9
|
register_prompt: "Don't have an account?",
|
|
15
10
|
login_prompt: "Already have an account?",
|
|
16
11
|
login: "Login",
|
|
17
|
-
|
|
12
|
+
button_continue: "Continue",
|
|
18
13
|
change_prompt: "Forgot password?",
|
|
19
14
|
code_resend: "Resend code",
|
|
20
15
|
code_return: "Back to",
|
|
@@ -33,37 +28,16 @@ const PasskeyUI = (options) => {
|
|
|
33
28
|
window.addEventListener("load", async () => {
|
|
34
29
|
const { startAuthentication } = SimpleWebAuthnBrowser;
|
|
35
30
|
const authorizeForm = document.getElementById("authorizeForm");
|
|
31
|
+
const message = document.querySelector("[data-slot='message']");
|
|
36
32
|
const origin = window.location.origin;
|
|
37
33
|
const rpID = window.location.hostname;
|
|
38
34
|
|
|
39
|
-
const showMessage = (msg) => {
|
|
40
|
-
const messageEl = document.querySelector("[data-slot='message']");
|
|
41
|
-
if (messageEl) {
|
|
42
|
-
messageEl.innerHTML = msg;
|
|
43
|
-
} else {
|
|
44
|
-
// Create alert if it doesn't exist
|
|
45
|
-
const alertDiv = document.createElement("div");
|
|
46
|
-
alertDiv.setAttribute("data-component", "form-alert");
|
|
47
|
-
alertDiv.setAttribute("role", "alert");
|
|
48
|
-
alertDiv.setAttribute("aria-live", "polite");
|
|
49
|
-
alertDiv.setAttribute("data-color", "error");
|
|
50
|
-
alertDiv.innerHTML = '<span data-slot="message">' + msg + '</span>';
|
|
51
|
-
authorizeForm.insertBefore(alertDiv, authorizeForm.firstChild);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const clearMessage = () => {
|
|
56
|
-
const alertDiv = document.querySelector("[data-component='form-alert']");
|
|
57
|
-
if (alertDiv) {
|
|
58
|
-
alertDiv.remove();
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
35
|
authorizeForm.addEventListener("submit", async (e) => {
|
|
63
36
|
e.preventDefault();
|
|
64
37
|
const formData = new FormData(authorizeForm);
|
|
65
38
|
const email = formData.get("email");
|
|
66
|
-
|
|
39
|
+
|
|
40
|
+
message.textContent = "";
|
|
67
41
|
|
|
68
42
|
// GET authentication options from the endpoint that calls
|
|
69
43
|
// @simplewebauthn/server -> generateAuthenticationOptions()
|
|
@@ -74,7 +48,7 @@ const PasskeyUI = (options) => {
|
|
|
74
48
|
const optionsJSON = await resp.json();
|
|
75
49
|
|
|
76
50
|
if (optionsJSON.error) {
|
|
77
|
-
|
|
51
|
+
message.textContent = optionsJSON.error;
|
|
78
52
|
return;
|
|
79
53
|
}
|
|
80
54
|
|
|
@@ -83,7 +57,7 @@ const PasskeyUI = (options) => {
|
|
|
83
57
|
// Pass the options to the authenticator and wait for a response
|
|
84
58
|
attResp = await startAuthentication({ optionsJSON });
|
|
85
59
|
} catch (error) {
|
|
86
|
-
|
|
60
|
+
message.textContent = error;
|
|
87
61
|
throw error;
|
|
88
62
|
}
|
|
89
63
|
|
|
@@ -116,25 +90,27 @@ const PasskeyUI = (options) => {
|
|
|
116
90
|
);
|
|
117
91
|
try {
|
|
118
92
|
const errorData = await verificationResp.json();
|
|
119
|
-
|
|
93
|
+
message.textContent = errorData.error;
|
|
120
94
|
} catch (error) {
|
|
121
|
-
|
|
95
|
+
message.textContent = "Something went wrong";
|
|
122
96
|
}
|
|
123
97
|
}
|
|
124
98
|
});
|
|
125
99
|
});
|
|
126
100
|
` } }),
|
|
127
|
-
/* @__PURE__ */ jsx("h1", { children: copy.authorize_title }),
|
|
128
|
-
/* @__PURE__ */ jsx("p", { children: copy.authorize_description }),
|
|
129
101
|
/* @__PURE__ */ jsxs("form", {
|
|
130
102
|
id: "authorizeForm",
|
|
131
103
|
"data-component": "form",
|
|
132
104
|
children: [
|
|
133
105
|
/* @__PURE__ */ jsx(FormAlert, {}),
|
|
134
106
|
/* @__PURE__ */ jsx("input", {
|
|
107
|
+
id: "auth-email",
|
|
135
108
|
"data-component": "input",
|
|
136
109
|
type: "email",
|
|
137
110
|
name: "email",
|
|
111
|
+
"aria-required": "true",
|
|
112
|
+
"aria-describedby": "auth-email-help",
|
|
113
|
+
autoComplete: "email",
|
|
138
114
|
required: true,
|
|
139
115
|
placeholder: copy.input_email
|
|
140
116
|
}),
|
|
@@ -142,7 +118,7 @@ const PasskeyUI = (options) => {
|
|
|
142
118
|
type: "submit",
|
|
143
119
|
id: "btnLogin",
|
|
144
120
|
"data-component": "button",
|
|
145
|
-
children: copy.
|
|
121
|
+
children: copy.button_continue
|
|
146
122
|
}),
|
|
147
123
|
/* @__PURE__ */ jsx("div", {
|
|
148
124
|
"data-component": "form-footer",
|
|
@@ -171,37 +147,15 @@ const PasskeyUI = (options) => {
|
|
|
171
147
|
window.addEventListener("load", async () => {
|
|
172
148
|
const { startRegistration } = SimpleWebAuthnBrowser;
|
|
173
149
|
const registerForm = document.getElementById("registerForm");
|
|
150
|
+
const message = document.querySelector("[data-slot='message']");
|
|
174
151
|
const origin = window.location.origin;
|
|
175
152
|
const rpID = window.location.hostname;
|
|
176
153
|
|
|
177
|
-
const showMessage = (msg) => {
|
|
178
|
-
const messageEl = document.querySelector("[data-slot='message']");
|
|
179
|
-
if (messageEl) {
|
|
180
|
-
messageEl.innerHTML = msg;
|
|
181
|
-
} else {
|
|
182
|
-
// Create alert if it doesn't exist
|
|
183
|
-
const alertDiv = document.createElement("div");
|
|
184
|
-
alertDiv.setAttribute("data-component", "form-alert");
|
|
185
|
-
alertDiv.setAttribute("role", "alert");
|
|
186
|
-
alertDiv.setAttribute("aria-live", "polite");
|
|
187
|
-
alertDiv.setAttribute("data-color", "error");
|
|
188
|
-
alertDiv.innerHTML = '<span data-slot="message">' + msg + '</span>';
|
|
189
|
-
registerForm.insertBefore(alertDiv, registerForm.firstChild);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const clearMessage = () => {
|
|
194
|
-
const alertDiv = document.querySelector("[data-component='form-alert']");
|
|
195
|
-
if (alertDiv) {
|
|
196
|
-
alertDiv.remove();
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
154
|
// Start registration when the user clicks a button
|
|
201
155
|
const register = async (otherDevice = false) => {
|
|
202
156
|
const formData = new FormData(registerForm);
|
|
203
157
|
const email = formData.get("email");
|
|
204
|
-
|
|
158
|
+
message.textContent = "";
|
|
205
159
|
|
|
206
160
|
// GET registration options from the endpoint that calls
|
|
207
161
|
// @simplewebauthn/server -> generateRegistrationOptions()
|
|
@@ -218,7 +172,7 @@ const PasskeyUI = (options) => {
|
|
|
218
172
|
const optionsJSON = await resp.json();
|
|
219
173
|
|
|
220
174
|
if (optionsJSON.error) {
|
|
221
|
-
|
|
175
|
+
message.textContent = optionsJSON.error;
|
|
222
176
|
return;
|
|
223
177
|
}
|
|
224
178
|
|
|
@@ -227,7 +181,7 @@ const PasskeyUI = (options) => {
|
|
|
227
181
|
// Pass the options to the authenticator and wait for a response
|
|
228
182
|
attResp = await startRegistration({ optionsJSON });
|
|
229
183
|
} catch (error) {
|
|
230
|
-
|
|
184
|
+
message.textContent = error;
|
|
231
185
|
throw error;
|
|
232
186
|
}
|
|
233
187
|
|
|
@@ -263,14 +217,14 @@ const PasskeyUI = (options) => {
|
|
|
263
217
|
);
|
|
264
218
|
try {
|
|
265
219
|
const errorData = await verificationResp.json();
|
|
266
|
-
|
|
220
|
+
message.textContent = errorData.error;
|
|
267
221
|
} catch (error) {
|
|
268
|
-
|
|
222
|
+
message.textContent = "Something went wrong";
|
|
269
223
|
}
|
|
270
224
|
}
|
|
271
225
|
} catch (error) {
|
|
272
226
|
console.error(error);
|
|
273
|
-
|
|
227
|
+
message.textContent = "Something went wrong";
|
|
274
228
|
}
|
|
275
229
|
};
|
|
276
230
|
|
|
@@ -280,17 +234,19 @@ const PasskeyUI = (options) => {
|
|
|
280
234
|
});
|
|
281
235
|
});
|
|
282
236
|
` } }),
|
|
283
|
-
/* @__PURE__ */ jsx("h1", { children: copy.register_title }),
|
|
284
|
-
/* @__PURE__ */ jsx("p", { children: copy.register_description }),
|
|
285
237
|
/* @__PURE__ */ jsxs("form", {
|
|
286
238
|
id: "registerForm",
|
|
287
239
|
"data-component": "form",
|
|
288
240
|
children: [
|
|
289
241
|
/* @__PURE__ */ jsx(FormAlert, {}),
|
|
290
242
|
/* @__PURE__ */ jsx("input", {
|
|
243
|
+
id: "reg-email",
|
|
291
244
|
"data-component": "input",
|
|
292
245
|
type: "email",
|
|
293
246
|
name: "email",
|
|
247
|
+
"aria-required": "true",
|
|
248
|
+
"aria-describedby": "reg-email-help",
|
|
249
|
+
autoComplete: "email",
|
|
294
250
|
required: true,
|
|
295
251
|
placeholder: copy.input_email
|
|
296
252
|
}),
|
|
@@ -298,7 +254,7 @@ const PasskeyUI = (options) => {
|
|
|
298
254
|
"data-component": "button",
|
|
299
255
|
type: "submit",
|
|
300
256
|
id: "btnRegister",
|
|
301
|
-
children: copy.
|
|
257
|
+
children: copy.button_continue
|
|
302
258
|
}),
|
|
303
259
|
/* @__PURE__ */ jsx("button", {
|
|
304
260
|
"data-component": "button",
|
package/dist/ui/password.js
CHANGED