tosijs-ui 1.0.0

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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/ab-test.d.ts +14 -0
  4. package/dist/ab-test.js +116 -0
  5. package/dist/babylon-3d.d.ts +53 -0
  6. package/dist/babylon-3d.js +292 -0
  7. package/dist/bodymovin-player.d.ts +32 -0
  8. package/dist/bodymovin-player.js +172 -0
  9. package/dist/bp-loader.d.ts +1 -0
  10. package/dist/bp-loader.js +26 -0
  11. package/dist/carousel.d.ts +113 -0
  12. package/dist/carousel.js +308 -0
  13. package/dist/code-editor.d.ts +27 -0
  14. package/dist/code-editor.js +102 -0
  15. package/dist/color-input.d.ts +41 -0
  16. package/dist/color-input.js +112 -0
  17. package/dist/data-table.d.ts +79 -0
  18. package/dist/data-table.js +774 -0
  19. package/dist/drag-and-drop.d.ts +2 -0
  20. package/dist/drag-and-drop.js +386 -0
  21. package/dist/editable-rect.d.ts +97 -0
  22. package/dist/editable-rect.js +450 -0
  23. package/dist/filter-builder.d.ts +64 -0
  24. package/dist/filter-builder.js +468 -0
  25. package/dist/float.d.ts +18 -0
  26. package/dist/float.js +170 -0
  27. package/dist/form.d.ts +68 -0
  28. package/dist/form.js +466 -0
  29. package/dist/gamepad.d.ts +34 -0
  30. package/dist/gamepad.js +115 -0
  31. package/dist/icon-data.d.ts +312 -0
  32. package/dist/icon-data.js +308 -0
  33. package/dist/icon-types.d.ts +7 -0
  34. package/dist/icon-types.js +1 -0
  35. package/dist/icons.d.ts +17 -0
  36. package/dist/icons.js +374 -0
  37. package/dist/iife.js +69 -0
  38. package/dist/iife.js.map +49 -0
  39. package/dist/index-iife.d.ts +1 -0
  40. package/dist/index-iife.js +4 -0
  41. package/dist/index.d.ts +37 -0
  42. package/dist/index.js +37 -0
  43. package/dist/index.js.map +47 -0
  44. package/dist/live-example.d.ts +63 -0
  45. package/dist/live-example.js +611 -0
  46. package/dist/localize.d.ts +46 -0
  47. package/dist/localize.js +381 -0
  48. package/dist/make-sorter.d.ts +3 -0
  49. package/dist/make-sorter.js +119 -0
  50. package/dist/make-sorter.test.d.ts +1 -0
  51. package/dist/make-sorter.test.js +48 -0
  52. package/dist/mapbox.d.ts +24 -0
  53. package/dist/mapbox.js +161 -0
  54. package/dist/markdown-viewer.d.ts +17 -0
  55. package/dist/markdown-viewer.js +173 -0
  56. package/dist/match-shortcut.d.ts +9 -0
  57. package/dist/match-shortcut.js +13 -0
  58. package/dist/match-shortcut.test.d.ts +1 -0
  59. package/dist/match-shortcut.test.js +194 -0
  60. package/dist/menu.d.ts +60 -0
  61. package/dist/menu.js +614 -0
  62. package/dist/notifications.d.ts +106 -0
  63. package/dist/notifications.js +308 -0
  64. package/dist/password-strength.d.ts +35 -0
  65. package/dist/password-strength.js +302 -0
  66. package/dist/playwright.config.d.ts +9 -0
  67. package/dist/playwright.config.js +73 -0
  68. package/dist/pop-float.d.ts +10 -0
  69. package/dist/pop-float.js +231 -0
  70. package/dist/rating.d.ts +62 -0
  71. package/dist/rating.js +192 -0
  72. package/dist/rich-text.d.ts +35 -0
  73. package/dist/rich-text.js +296 -0
  74. package/dist/segmented.d.ts +80 -0
  75. package/dist/segmented.js +298 -0
  76. package/dist/select.d.ts +43 -0
  77. package/dist/select.js +427 -0
  78. package/dist/side-nav.d.ts +36 -0
  79. package/dist/side-nav.js +106 -0
  80. package/dist/size-break.d.ts +18 -0
  81. package/dist/size-break.js +118 -0
  82. package/dist/sizer.d.ts +34 -0
  83. package/dist/sizer.js +92 -0
  84. package/dist/src/ab-test.d.ts +14 -0
  85. package/dist/src/babylon-3d.d.ts +53 -0
  86. package/dist/src/bodymovin-player.d.ts +32 -0
  87. package/dist/src/bp-loader.d.ts +0 -0
  88. package/dist/src/carousel.d.ts +113 -0
  89. package/dist/src/code-editor.d.ts +27 -0
  90. package/dist/src/color-input.d.ts +41 -0
  91. package/dist/src/data-table.d.ts +79 -0
  92. package/dist/src/drag-and-drop.d.ts +2 -0
  93. package/dist/src/editable-rect.d.ts +97 -0
  94. package/dist/src/filter-builder.d.ts +64 -0
  95. package/dist/src/float.d.ts +18 -0
  96. package/dist/src/form.d.ts +68 -0
  97. package/dist/src/gamepad.d.ts +34 -0
  98. package/dist/src/icon-data.d.ts +309 -0
  99. package/dist/src/icon-types.d.ts +7 -0
  100. package/dist/src/icons.d.ts +17 -0
  101. package/dist/src/index.d.ts +37 -0
  102. package/dist/src/live-example.d.ts +51 -0
  103. package/dist/src/localize.d.ts +30 -0
  104. package/dist/src/make-sorter.d.ts +3 -0
  105. package/dist/src/mapbox.d.ts +24 -0
  106. package/dist/src/markdown-viewer.d.ts +15 -0
  107. package/dist/src/match-shortcut.d.ts +9 -0
  108. package/dist/src/menu.d.ts +60 -0
  109. package/dist/src/notifications.d.ts +106 -0
  110. package/dist/src/password-strength.d.ts +35 -0
  111. package/dist/src/pop-float.d.ts +10 -0
  112. package/dist/src/rating.d.ts +62 -0
  113. package/dist/src/rich-text.d.ts +28 -0
  114. package/dist/src/segmented.d.ts +80 -0
  115. package/dist/src/select.d.ts +43 -0
  116. package/dist/src/side-nav.d.ts +36 -0
  117. package/dist/src/size-break.d.ts +18 -0
  118. package/dist/src/sizer.d.ts +34 -0
  119. package/dist/src/tab-selector.d.ts +91 -0
  120. package/dist/src/tag-list.d.ts +37 -0
  121. package/dist/src/track-drag.d.ts +5 -0
  122. package/dist/src/version.d.ts +1 -0
  123. package/dist/src/via-tag.d.ts +2 -0
  124. package/dist/tab-selector.d.ts +91 -0
  125. package/dist/tab-selector.js +326 -0
  126. package/dist/tag-list.d.ts +37 -0
  127. package/dist/tag-list.js +375 -0
  128. package/dist/track-drag.d.ts +5 -0
  129. package/dist/track-drag.js +143 -0
  130. package/dist/version.d.ts +1 -0
  131. package/dist/version.js +1 -0
  132. package/dist/via-tag.d.ts +2 -0
  133. package/dist/via-tag.js +102 -0
  134. package/package.json +58 -0
@@ -0,0 +1,308 @@
1
+ /*#
2
+
3
+ # notifications
4
+
5
+ `XinNotification` provides a singleton custom `<xin-notification>` element that manages
6
+ a list of notifications.
7
+
8
+ The notifications are displayed most-recent first. If the notifications would take more than
9
+ half the height of the display, they are scrolled.
10
+
11
+ You can post a notification simply with `XinNotification.post()` or `postNotification()`.
12
+
13
+ ```
14
+ interface NotificationSpec {
15
+ message: string
16
+ type?: 'success' | 'info' | 'log' | 'warn' | 'error' | 'progress' // default 'info'
17
+ icon?: SVGElement | string // defaults to an info icon
18
+ duration?: number
19
+ progress?: () => number // return percentage completion
20
+ close?: () => void
21
+ }
22
+ ```
23
+
24
+ If you provide a `progress` callback (which is assumed to return a number from `0-100`, with
25
+ 100+ indicating completion) then `XinNotification` will poll it every second until the
26
+ task completes or the notification is closed. Returning 100 or more will automatically close
27
+ the notification.
28
+
29
+ If you configure a notification's `type = "progress"` but don't provide a `progress` callback
30
+ then an indefinite `<progress>` element will be displayed.
31
+
32
+ If you provide a `close` callback, it will be fired if the user closes the notification.
33
+
34
+ `postNotification` returns a callback function that closes the note programmatically (e.g.
35
+ when an operation completes). This will *also* call any `close` callback function you
36
+ provided. (The progress demos in the example exercise this functionality.)
37
+
38
+ ```js
39
+ const { postNotification, icons } = xinjsui
40
+
41
+ const form = preview.querySelector('xin-form')
42
+ const submit = preview.querySelector('.submit')
43
+ const closeButton = preview.querySelector('.close')
44
+
45
+ let close
46
+
47
+ form.submitCallback = (value, isValid) => {
48
+ if (!isValid) return
49
+ if (value.type.startsWith('progress')) {
50
+ startTime = Date.now()
51
+ const { message, duration, icon } = value
52
+ close = postNotification({
53
+ message,
54
+ type: 'progress',
55
+ icon,
56
+ progress: value.type === 'progress' ? () => (Date.now() - startTime) / (10 * duration) : undefined,
57
+ close: () => { postNotification(`${value.message} cancelled`) },
58
+ })
59
+ } else {
60
+ close = postNotification(value)
61
+ }
62
+ console.log(close)
63
+ closeButton.disabled = false
64
+ }
65
+
66
+ submit.addEventListener('click', form.submit)
67
+ closeButton.addEventListener('click', () => {
68
+ if (close) {
69
+ close()
70
+ }
71
+ })
72
+
73
+ postNotification({
74
+ message: 'Welcome to xinjs-ui notifications, this message will disappear in 2s',
75
+ duration: 2
76
+ })
77
+ ```
78
+ ```html
79
+ <xin-form>
80
+ <h3 slot="header">Notification Test</h3>
81
+ <xin-field caption="Message" key="message" type="string" value="This is a test…"></xin-field>
82
+ <xin-field caption="Type" key="type" value="info">
83
+ <xin-select slot="input"
84
+ options="error,warn,info,success,log,,progress,progress (indefinite)"
85
+ ></xin-select>
86
+ </xin-field>
87
+ <xin-field caption="Icon" key="icon" value="info">
88
+ <xin-select slot="input"
89
+ options="info,bug,thumbsUp,thumbsDown,message"
90
+ ></xin-select>
91
+ </xin-field>
92
+ <xin-field caption="Duration" key="duration" type="number" value="2"></xin-field>
93
+ <button slot="footer" class="close" disabled>Close Last Notification</button>
94
+ <span slot="footer" class="elastic"></span>
95
+ <button slot="footer" class="submit">Post Notification</button>
96
+ </xin-form>
97
+ ```
98
+ ```css
99
+ xin-form {
100
+ height: 100%;
101
+ }
102
+
103
+ xin-form::part(content) {
104
+ display: flex;
105
+ flex-direction: column;
106
+ padding: 10px;
107
+ gap: 10px;
108
+ background: var(--background);
109
+ }
110
+
111
+ xin-form::part(header),
112
+ xin-form::part(footer) {
113
+ background: #eee;
114
+ justify-content: center;
115
+ padding: 10px;
116
+ }
117
+
118
+ xin-form h3 {
119
+ margin: 0;
120
+ }
121
+
122
+ xin-form label {
123
+ display: grid;
124
+ grid-template-columns: 120px 1fr;
125
+ }
126
+ ```
127
+
128
+ ## `postNotification(spec: NotificationSpec | string)`
129
+
130
+ This is simply a wrapper for `XinNotification.post()`.
131
+ */
132
+ import { Component, elements, vars } from 'xinjs';
133
+ import { icons } from './icons';
134
+ import { findHighestZ } from './track-drag';
135
+ const { div, button } = elements;
136
+ const COLOR_MAP = {
137
+ error: 'red',
138
+ warn: 'orange',
139
+ info: 'royalblue',
140
+ log: 'gray',
141
+ success: 'green',
142
+ progress: 'royalblue',
143
+ };
144
+ export class XinNotification extends Component {
145
+ static singleton;
146
+ static styleSpec = {
147
+ ':host': {
148
+ _notificationSpacing: 8,
149
+ _notificationWidth: 360,
150
+ _notificationPadding: `${vars.notificationSpacing} ${vars.notificationSpacing50} ${vars.notificationSpacing} ${vars.notificationSpacing200}`,
151
+ _notificationBg: '#fafafa',
152
+ _notificationAccentColor: '#aaa',
153
+ _notificationTextColor: '#444',
154
+ _notificationIconSize: vars.notificationSpacing300,
155
+ _notificationButtonSize: 48,
156
+ _notificationBorderWidth: '3px 0 0',
157
+ _notificationBorderRadius: vars.notificationSpacing50,
158
+ position: 'fixed',
159
+ left: 0,
160
+ right: 0,
161
+ bottom: 0,
162
+ paddingBottom: vars.notificationSpacing,
163
+ width: vars.notificationWidth,
164
+ display: 'flex',
165
+ flexDirection: 'column-reverse',
166
+ margin: '0 auto',
167
+ gap: vars.notificationSpacing,
168
+ maxHeight: '50vh',
169
+ overflow: 'hidden auto',
170
+ boxShadow: 'none !important',
171
+ },
172
+ ':host *': {
173
+ color: vars.notificationTextColor,
174
+ },
175
+ ':host .note': {
176
+ display: 'grid',
177
+ background: vars.notificationBg,
178
+ padding: vars.notificationPadding,
179
+ gridTemplateColumns: `${vars.notificationIconSize} 1fr ${vars.notificationButtonSize}`,
180
+ gap: vars.notificationSpacing,
181
+ alignItems: 'center',
182
+ borderRadius: vars.notificationBorderRadius,
183
+ boxShadow: `0 2px 8px #0006, inset 0 0 0 2px ${vars.notificationAccentColor}`,
184
+ borderColor: vars.notificationAccentColor,
185
+ borderWidth: vars.notificationBorderWidth,
186
+ borderStyle: 'solid',
187
+ transition: '0.5s ease-in',
188
+ transitionProperty: 'margin, opacity',
189
+ zIndex: 1,
190
+ },
191
+ ':host .note .icon': {
192
+ stroke: vars.notificationAccentColor,
193
+ },
194
+ ':host .note button': {
195
+ display: 'flex',
196
+ lineHeight: vars.notificationButtonSize,
197
+ padding: 0,
198
+ margin: 0,
199
+ height: vars.notificationButtonSize,
200
+ width: vars.notificationButtonSize,
201
+ background: 'transparent',
202
+ alignItems: 'center',
203
+ justifyContent: 'center',
204
+ boxShadow: 'none',
205
+ border: 'none',
206
+ position: 'relative',
207
+ },
208
+ ':host .note button:hover svg': {
209
+ stroke: vars.notificationAccentColor,
210
+ },
211
+ ':host .note button:active svg': {
212
+ borderRadius: 99,
213
+ stroke: vars.notificationBg,
214
+ background: vars.notificationAccentColor,
215
+ padding: vars.spacing50,
216
+ },
217
+ ':host .note svg': {
218
+ height: vars.notificationIconSize,
219
+ width: vars.notificationIconSize,
220
+ pointerEvents: 'none',
221
+ },
222
+ ':host .message': {
223
+ display: 'flex',
224
+ flexDirection: 'column',
225
+ alignItems: 'center',
226
+ gap: vars.notificationSpacing,
227
+ },
228
+ ':host .note.closing': {
229
+ opacity: 0,
230
+ zIndex: 0,
231
+ },
232
+ };
233
+ static removeNote(note) {
234
+ note.classList.add('closing');
235
+ note.style.marginBottom = -note.offsetHeight + 'px';
236
+ const remove = () => {
237
+ note.remove();
238
+ };
239
+ note.addEventListener('transitionend', remove);
240
+ setTimeout(remove, 1000);
241
+ }
242
+ static post(spec) {
243
+ const { message, duration, type, close, progress, icon } = Object.assign({ type: 'info', duration: -1 }, typeof spec === 'string' ? { message: spec } : spec);
244
+ if (!this.singleton) {
245
+ this.singleton = xinNotification();
246
+ }
247
+ const singleton = this.singleton;
248
+ document.body.append(singleton);
249
+ singleton.style.zIndex = String(findHighestZ() + 1);
250
+ const _notificationAccentColor = COLOR_MAP[type];
251
+ const progressBar = progress || type === 'progress' ? elements.progress() : {};
252
+ const closeCallback = () => {
253
+ if (close) {
254
+ close();
255
+ }
256
+ XinNotification.removeNote(note);
257
+ };
258
+ const iconElement = icon instanceof SVGElement
259
+ ? icon
260
+ : icon
261
+ ? icons[icon]({ class: 'icon' })
262
+ : icons.info({ class: 'icon' });
263
+ const note = div({
264
+ class: `note ${type}`,
265
+ style: {
266
+ _notificationAccentColor,
267
+ },
268
+ }, iconElement, div({ class: 'message' }, div(message), progressBar), button({
269
+ class: 'close',
270
+ title: 'close',
271
+ // we can't use onClick because this lives inside a shadowDOM
272
+ apply(elt) {
273
+ elt.addEventListener('click', closeCallback);
274
+ },
275
+ }, icons.x()));
276
+ singleton.shadowRoot.append(note);
277
+ if (progressBar instanceof HTMLProgressElement &&
278
+ progress instanceof Function) {
279
+ progressBar.setAttribute('max', String(100));
280
+ progressBar.value = progress();
281
+ const interval = setInterval(() => {
282
+ if (!singleton.shadowRoot.contains(note)) {
283
+ clearInterval(interval);
284
+ return;
285
+ }
286
+ const percentage = progress();
287
+ progressBar.value = percentage;
288
+ if (percentage >= 100) {
289
+ XinNotification.removeNote(note);
290
+ }
291
+ }, 1000);
292
+ }
293
+ if (duration > 0) {
294
+ setTimeout(() => {
295
+ XinNotification.removeNote(note);
296
+ }, duration * 1000);
297
+ }
298
+ note.scrollIntoView();
299
+ return closeCallback;
300
+ }
301
+ content = null;
302
+ }
303
+ export const xinNotification = XinNotification.elementCreator({
304
+ tag: 'xin-notification',
305
+ });
306
+ export function postNotification(spec) {
307
+ return XinNotification.post(spec);
308
+ }
@@ -0,0 +1,35 @@
1
+ import { Component } from 'tosijs';
2
+ export declare const digest: (s: string, method?: string) => Promise<string>;
3
+ export declare const isBreached: (password: string) => Promise<boolean>;
4
+ export declare class XinPasswordStrength extends Component {
5
+ minLength: number;
6
+ goodLength: number;
7
+ indicatorColors: string;
8
+ descriptionColors: string;
9
+ issues: {
10
+ tooShort: boolean;
11
+ short: boolean;
12
+ noUpper: boolean;
13
+ noLower: boolean;
14
+ noNumber: boolean;
15
+ noSpecial: boolean;
16
+ };
17
+ issueDescriptions: {
18
+ tooShort: string;
19
+ short: string;
20
+ noUpper: string;
21
+ noLower: string;
22
+ noNumber: string;
23
+ noSpecial: string;
24
+ };
25
+ value: number;
26
+ strengthDescriptions: string[];
27
+ constructor();
28
+ strength(password: string): number;
29
+ isBreached(): Promise<boolean>;
30
+ updateIndicator: (password: string) => void;
31
+ update: (event: Event) => void;
32
+ content: () => any[];
33
+ render(): void;
34
+ }
35
+ export declare const xinPasswordStrength: import("tosijs").ElementCreator<Component<import("tosijs").PartsMap>>;
@@ -0,0 +1,302 @@
1
+ /*#
2
+ # password strength
3
+
4
+ Just wrap it a `<xin-password-strength>` element around an `<input>`
5
+ and it will gauge its content strength as a password. It will also
6
+ let you **securely verify** that the password hasn't been breached.
7
+
8
+ ```js
9
+ const { xinLocalized, localize } = xinjsui
10
+
11
+ const toggle = preview.querySelector('.toggle')
12
+ const icon = preview.querySelector('xin-icon')
13
+ const input = preview.querySelector('input')
14
+ const breach = preview.querySelector('.breach')
15
+ const output = preview.querySelector('.output')
16
+ const passwordStrength = preview.querySelector('xin-password-strength')
17
+
18
+ // Localization Example
19
+ passwordStrength.append(xinLocalized({
20
+ refString: 'Yes',
21
+ localeChanged () {
22
+ this.parentElement.strengthDescriptions = [
23
+ 'unacceptable',
24
+ 'very weak',
25
+ 'weak',
26
+ 'moderate',
27
+ 'strong',
28
+ 'very strong',
29
+ ].map(localize)
30
+ this.parentElement.queueRender()
31
+ }
32
+ }))
33
+
34
+ toggle.addEventListener('click', () => {
35
+ if (icon.icon === 'eye') {
36
+ input.type = 'text'
37
+ icon.icon = 'eyeOff'
38
+ } else {
39
+ input.type = 'password'
40
+ icon.icon = 'eye'
41
+ }
42
+ })
43
+
44
+ breach.addEventListener('click', async () => {
45
+ preview.querySelector('xin-password-strength').isBreached().then(isBreached => {
46
+ output.textContent =
47
+ isBreached
48
+ ? 'This password has been breached, look at console for details'
49
+ : 'Seems OK'
50
+ output.classList.toggle('breached', isBreached)
51
+ })
52
+ })
53
+ ```
54
+ ```html
55
+ <xin-password-strength>
56
+ <input class="password" type="password">
57
+ <button class="toggle">
58
+ <xin-icon icon="eye"></xin-icon>
59
+ </button>
60
+ </xin-password-strength>
61
+
62
+ <br><br>
63
+ <button class="breach">
64
+ <xin-localized>Check if breached</xin-localized>
65
+ </button>
66
+ <div class="output"></div>
67
+ ```
68
+ ```css
69
+ input.password {
70
+ box-shadow: inset 0 0 0 2px var(--indicator-color);
71
+ }
72
+
73
+ .breached {
74
+ color: white;
75
+ background: red;
76
+ }
77
+ ```
78
+
79
+ ## Algorithm
80
+
81
+ The password is assessed to have a strength based on:
82
+
83
+ - **length** one point for at least `goodLength` characters long.
84
+ - **[a-z]** one point for containing a lowercase letter
85
+ - **[A-Z]** one point for containing an uppercase letter
86
+ - **[0-9]** one point for containing a numeric character
87
+ - **^[a-zA-Z0-9]]** one point for containing some other kind of character
88
+
89
+ A password smaller than `minLength` is an automatic `0`.
90
+
91
+ ## Attributes
92
+
93
+ - `minLength` defaults to `8`
94
+ - `goodLength` defaults to `12`
95
+ - `indicatorColors` six HTML colors, separated by commas, defaults to `'#f00,#f40,#f80,#ef0,#8f0,#0d4'`
96
+ - `descriptionColors` six HTML colors, sepeated by commans, defaults to `'#000,#000,#000,#000,#000,#fff'`
97
+
98
+ ## Properties
99
+
100
+ - `value`, `strength` is a number from 0 to 5
101
+ - `issues` is a structure which you can use to generate feedback
102
+
103
+ ```
104
+ <xin-password-strength>.issues = {
105
+ tooShort: boolean,
106
+ short: boolean,
107
+ noUpper: boolean,
108
+ noLower: boolean,
109
+ noNumber: boolean,
110
+ noSpecial: boolean,
111
+ }
112
+ ```
113
+
114
+ ## Customizing / Localizing Strings
115
+
116
+ The following properties control the feedback generated.
117
+
118
+ ```
119
+ issueDescriptions = {
120
+ tooShort: 'too short',
121
+ short: 'short',
122
+ noUpper: 'no upper case',
123
+ noLower: 'no lower case',
124
+ noNumber: 'no digits',
125
+ noSpecial: 'no unusual characters',
126
+ }
127
+ ```
128
+
129
+ ```
130
+ strengthDescriptions = [
131
+ 'unacceptable',
132
+ 'very weak',
133
+ 'weak',
134
+ 'moderate',
135
+ 'strong',
136
+ 'very strong',
137
+ ]
138
+ ```
139
+
140
+ ## `isBreached()`
141
+
142
+ `<xin-password-meter>` also provides an `isBreached(): Promise<boolean>` method
143
+ which uses [weakpass.com's API](https://weakpass.com/) to tell you if the password has been
144
+ breached.
145
+
146
+ > Note that `isBreached` does not send the plain-text password anywhere. It uses **SHA-1**
147
+ to hash the password and then sends that for lookup.
148
+
149
+ ## Utility Functions
150
+
151
+ Two functions used internally for querying [Weakpass,com](https://weakpass.com/) are
152
+ provided in case they're useful on their own.
153
+
154
+ `isBreached(password: striing): Promise<boolean>` will return `true` if the password is
155
+ found in Weakpass's database (and spit out extra info to the console).
156
+
157
+ `digest(s: string, method="sha-1"): Promise<string>` is just a nice wrapper for `crypto.digest`.
158
+ */
159
+ import { Component, elements, vars, varDefault } from 'xinjs';
160
+ export const digest = async (s, method = 'SHA-1') => {
161
+ // Convert password to an ArrayBuffer
162
+ const encoder = new TextEncoder();
163
+ const data = encoder.encode(s);
164
+ // Hash the password using SHA-1
165
+ const hashBuffer = await crypto.subtle.digest(method, data);
166
+ // Convert the hash to a hexadecimal string
167
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
168
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
169
+ return hashHex;
170
+ };
171
+ export const isBreached = async (password) => {
172
+ const hashHex = await digest(password);
173
+ const response = await fetch(`https://weakpass.com/api/v1/search/${hashHex}`);
174
+ if (response.ok) {
175
+ const result = await response.json();
176
+ console.log('password found in weakpass database', result);
177
+ }
178
+ return response.status !== 404;
179
+ };
180
+ const { span, xinSlot } = elements;
181
+ export class XinPasswordStrength extends Component {
182
+ minLength = 8;
183
+ goodLength = 12;
184
+ indicatorColors = '#f00,#f40,#f80,#ef0,#8f0,#0a2';
185
+ descriptionColors = '#000,#000,#000,#000,#000,#fff';
186
+ issues = {
187
+ tooShort: true,
188
+ short: true,
189
+ noUpper: true,
190
+ noLower: true,
191
+ noNumber: true,
192
+ noSpecial: true,
193
+ };
194
+ issueDescriptions = {
195
+ tooShort: 'too short',
196
+ short: 'short',
197
+ noUpper: 'no upper case',
198
+ noLower: 'no lower case',
199
+ noNumber: 'no digits',
200
+ noSpecial: 'no unusual characters',
201
+ };
202
+ value = 0;
203
+ strengthDescriptions = [
204
+ 'unacceptable',
205
+ 'very weak',
206
+ 'weak',
207
+ 'moderate',
208
+ 'strong',
209
+ 'very strong',
210
+ ];
211
+ constructor() {
212
+ super();
213
+ this.initAttributes('minLength', 'goodLength', 'indicatorColors');
214
+ }
215
+ strength(password) {
216
+ this.issues = {
217
+ tooShort: password.length < this.minLength,
218
+ short: password.length < this.goodLength,
219
+ noUpper: !password.match(/[A-Z]/),
220
+ noLower: !password.match(/[a-z]/),
221
+ noNumber: !password.match(/[0-9]/),
222
+ noSpecial: !password.match(/[^a-zA-Z0-9]/),
223
+ };
224
+ return this.issues.tooShort
225
+ ? 0
226
+ : Object.values(this.issues).filter((v) => !v).length - 1;
227
+ }
228
+ async isBreached() {
229
+ const password = this.querySelector('input')?.value;
230
+ if (!password || typeof password !== 'string') {
231
+ return true;
232
+ }
233
+ return await isBreached(password);
234
+ }
235
+ updateIndicator = (password) => {
236
+ const { level, description } = this.parts;
237
+ const colors = this.indicatorColors.split(',');
238
+ const descriptionColors = this.descriptionColors.split(',');
239
+ const strength = this.strength(password);
240
+ if (this.value !== strength) {
241
+ this.value = strength;
242
+ this.dispatchEvent(new Event('change'));
243
+ }
244
+ level.style.width = `${(strength + 1) * 16.67}%`;
245
+ this.style.setProperty('--indicator-color', colors[strength]);
246
+ this.style.setProperty('--description-color', descriptionColors[strength]);
247
+ description.textContent = this.strengthDescriptions[strength];
248
+ };
249
+ update = (event) => {
250
+ const input = event.target.closest('input');
251
+ this.updateIndicator(input?.value || '');
252
+ };
253
+ content = () => [
254
+ xinSlot({ onInput: this.update }),
255
+ span({ part: 'meter' }, span({ part: 'level' }), span({ part: 'description' })),
256
+ ];
257
+ render() {
258
+ super.render();
259
+ const input = this.querySelector('input');
260
+ this.updateIndicator(input?.value);
261
+ }
262
+ }
263
+ export const xinPasswordStrength = XinPasswordStrength.elementCreator({
264
+ tag: 'xin-password-strength',
265
+ styleSpec: {
266
+ ':host': {
267
+ display: 'inline-flex',
268
+ flexDirection: 'column',
269
+ gap: vars.spacing50,
270
+ position: 'relative',
271
+ },
272
+ ':host xin-slot': {
273
+ display: 'flex',
274
+ },
275
+ ':host [part="meter"]': {
276
+ display: 'block',
277
+ position: 'relative',
278
+ height: varDefault.meterHeight('24px'),
279
+ background: varDefault.indicatorBg('white'),
280
+ borderRadius: varDefault.meterRadius('4px'),
281
+ boxShadow: varDefault.meterShadow(`inset 0 0 0 2px ${vars.indicatorColor}`),
282
+ },
283
+ ':host [part="level"]': {
284
+ height: varDefault.levelHeight('20px'),
285
+ content: '" "',
286
+ display: 'inline-block',
287
+ width: 0,
288
+ transition: '0.15s ease-out',
289
+ background: vars.indicatorColor,
290
+ margin: varDefault.levelMargin('2px'),
291
+ borderRadius: varDefault.levelRadius('2px'),
292
+ },
293
+ ':host [part="description"]': {
294
+ position: 'absolute',
295
+ inset: '0',
296
+ color: vars.descriptionColor,
297
+ height: varDefault.meterHeight('24px'),
298
+ lineHeight: varDefault.meterHeight('24px'),
299
+ textAlign: 'center',
300
+ },
301
+ },
302
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Read environment variables from file.
3
+ * https://github.com/motdotla/dotenv
4
+ */
5
+ /**
6
+ * See https://playwright.dev/docs/test-configuration.
7
+ */
8
+ declare const _default: import("@playwright/test").PlaywrightTestConfig<{}, {}>;
9
+ export default _default;