@adia-ai/web-components 0.6.36 → 0.6.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -1
- package/components/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +2 -0
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/index.js +9 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/nav-group/nav-group.css +7 -1
- package/components/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/segmented/segmented.class.js +5 -1
- package/components/select/select.class.js +4 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +16 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +28 -6
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +3 -1
- package/components/tag/tag.a2ui.json +3 -2
- package/components/tag/tag.css +35 -11
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +35 -11
- package/components/tag/tag.yaml +13 -7
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -0
- package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +100 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +9 -0
- package/styles/resets.css +10 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<password-strength-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class without auto-registering the
|
|
5
|
+
* tag. Useful for test isolation, subclassing with tag-name override,
|
|
6
|
+
* or selective composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/password-strength`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <password-strength-ui></password-strength-ui>
|
|
16
|
+
*
|
|
17
|
+
* Layout:
|
|
18
|
+
* ▮▮▮▯ ← 4-segment bar, lit segments coloured per score
|
|
19
|
+
* Good ← optional text label
|
|
20
|
+
*
|
|
21
|
+
* Pair with `<input-ui type="password">`:
|
|
22
|
+
* input.addEventListener('input', e => meter.value = e.target.value);
|
|
23
|
+
*
|
|
24
|
+
* Score buckets (heuristic):
|
|
25
|
+
* 0 Weak — short or single-class
|
|
26
|
+
* 1 Fair — 8+ chars OR mixed classes
|
|
27
|
+
* 2 Good — 12+ chars + ≥3 classes
|
|
28
|
+
* 3 Strong — 16+ chars + 4 classes
|
|
29
|
+
*
|
|
30
|
+
* NOT zxcvbn — this is a tiny first-line heuristic. For security-
|
|
31
|
+
* critical surfaces, consumers can override the score by listening
|
|
32
|
+
* to `input` on the field and setting their own `data-score` on the
|
|
33
|
+
* meter, OR by extending this class.
|
|
34
|
+
*
|
|
35
|
+
* Security: `value` is JS-property only. NOT reflected to a DOM
|
|
36
|
+
* attribute; the password never appears in rendered HTML.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { UIElement } from '../../core/element.js';
|
|
40
|
+
|
|
41
|
+
const LABELS = ['Weak', 'Fair', 'Good', 'Strong'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute a 0..3 score for the given password string.
|
|
45
|
+
* Returns -1 for empty (sentinel — caller uses to grey out the bar).
|
|
46
|
+
*
|
|
47
|
+
* Scoring rubric:
|
|
48
|
+
* +1 length >= 8
|
|
49
|
+
* +1 length >= 12
|
|
50
|
+
* +1 length >= 16
|
|
51
|
+
* +1 ≥ 2 character classes (lower / upper / digit / symbol)
|
|
52
|
+
* +1 ≥ 3 character classes
|
|
53
|
+
* +1 all 4 character classes
|
|
54
|
+
* -1 contains 3+ same-char run (e.g. "aaa")
|
|
55
|
+
* -1 password is too short (< 8 chars) — clamps score to ≤ 0
|
|
56
|
+
*
|
|
57
|
+
* Clamped to [0, 3].
|
|
58
|
+
*/
|
|
59
|
+
function scorePassword(pwd) {
|
|
60
|
+
if (!pwd) return -1;
|
|
61
|
+
let score = 0;
|
|
62
|
+
if (pwd.length >= 8) score++;
|
|
63
|
+
if (pwd.length >= 12) score++;
|
|
64
|
+
if (pwd.length >= 16) score++;
|
|
65
|
+
|
|
66
|
+
const classes = [
|
|
67
|
+
/[a-z]/.test(pwd),
|
|
68
|
+
/[A-Z]/.test(pwd),
|
|
69
|
+
/[0-9]/.test(pwd),
|
|
70
|
+
/[^a-zA-Z0-9]/.test(pwd),
|
|
71
|
+
].filter(Boolean).length;
|
|
72
|
+
if (classes >= 2) score++;
|
|
73
|
+
if (classes >= 3) score++;
|
|
74
|
+
if (classes === 4) score++;
|
|
75
|
+
|
|
76
|
+
// Penalty: same-char run of 3+ (aaa, 111)
|
|
77
|
+
if (/(.)\1{2,}/.test(pwd)) score--;
|
|
78
|
+
|
|
79
|
+
// Hard floor for very short passwords — never above "Weak"
|
|
80
|
+
if (pwd.length < 8) score = Math.min(score, 0);
|
|
81
|
+
|
|
82
|
+
return Math.max(0, Math.min(3, score));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class UIPasswordStrength extends UIElement {
|
|
86
|
+
static properties = {
|
|
87
|
+
// JS-property only — no `reflect: true` so the password never
|
|
88
|
+
// appears in the DOM attribute set. `dynamic: true` keeps it out
|
|
89
|
+
// of any static-attribute extraction the a2ui pipeline does.
|
|
90
|
+
value: { type: String, default: '' },
|
|
91
|
+
minScore: { type: Number, default: 2, reflect: true, attribute: 'min-score' },
|
|
92
|
+
showLabel: { type: Boolean, default: true, reflect: true, attribute: 'show-label' },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
static template = () => null;
|
|
96
|
+
|
|
97
|
+
#lastEmittedScore = -2; // sentinel — different from any valid score / -1
|
|
98
|
+
|
|
99
|
+
connected() {
|
|
100
|
+
super.connected();
|
|
101
|
+
if (!this.querySelector('[slot="bar"]')) {
|
|
102
|
+
this.innerHTML = `
|
|
103
|
+
<div slot="bar" aria-hidden="true">
|
|
104
|
+
<div slot="segment" data-i="0"></div>
|
|
105
|
+
<div slot="segment" data-i="1"></div>
|
|
106
|
+
<div slot="segment" data-i="2"></div>
|
|
107
|
+
<div slot="segment" data-i="3"></div>
|
|
108
|
+
</div>
|
|
109
|
+
<span slot="label" aria-live="polite"></span>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
// ARIA meter semantics
|
|
113
|
+
this.setAttribute('role', 'meter');
|
|
114
|
+
this.setAttribute('aria-valuemin', '0');
|
|
115
|
+
this.setAttribute('aria-valuemax', '3');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
render() {
|
|
119
|
+
const score = scorePassword(this.value);
|
|
120
|
+
|
|
121
|
+
if (score < 0) {
|
|
122
|
+
this.removeAttribute('data-score');
|
|
123
|
+
this.removeAttribute('aria-valuenow');
|
|
124
|
+
this.setAttribute('aria-valuetext', 'Empty');
|
|
125
|
+
} else {
|
|
126
|
+
this.setAttribute('data-score', String(score));
|
|
127
|
+
this.setAttribute('aria-valuenow', String(score));
|
|
128
|
+
this.setAttribute('aria-valuetext', LABELS[score]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Update segment lit state via [data-lit]
|
|
132
|
+
const segments = this.querySelectorAll('[slot="segment"]');
|
|
133
|
+
segments.forEach((seg, i) => {
|
|
134
|
+
if (score >= 0 && i <= score) seg.setAttribute('data-lit', '');
|
|
135
|
+
else seg.removeAttribute('data-lit');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Update label text
|
|
139
|
+
const labelEl = this.querySelector('[slot="label"]');
|
|
140
|
+
if (labelEl) {
|
|
141
|
+
labelEl.textContent = score >= 0 && this.showLabel !== false ? LABELS[score] : '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Emit score-change only on bucket transitions
|
|
145
|
+
if (score !== this.#lastEmittedScore) {
|
|
146
|
+
this.#lastEmittedScore = score;
|
|
147
|
+
this.dispatchEvent(new CustomEvent('score-change', {
|
|
148
|
+
bubbles: true,
|
|
149
|
+
detail: {
|
|
150
|
+
score,
|
|
151
|
+
label: score >= 0 ? LABELS[score] : '',
|
|
152
|
+
satisfied: score >= this.minScore,
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
PASSWORD-STRENGTH-UI — 4-segment strength meter + label.
|
|
3
|
+
═══════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
@scope (password-strength-ui) {
|
|
6
|
+
:where(:scope) {
|
|
7
|
+
/* ── Layout ── */
|
|
8
|
+
--password-strength-gap-default: var(--a-space-1);
|
|
9
|
+
--password-strength-segment-height-default: 4px;
|
|
10
|
+
--password-strength-radius-default: var(--a-radius-full);
|
|
11
|
+
--password-strength-stack-gap-default: var(--a-space-1);
|
|
12
|
+
|
|
13
|
+
/* ── Colors ── */
|
|
14
|
+
--password-strength-segment-bg-default: var(--a-canvas-1-scrim);
|
|
15
|
+
--password-strength-color-weak-default: var(--a-danger-bg);
|
|
16
|
+
--password-strength-color-fair-default: var(--a-warning-bg);
|
|
17
|
+
--password-strength-color-good-default: var(--a-info-bg);
|
|
18
|
+
--password-strength-color-strong-default: var(--a-success-bg);
|
|
19
|
+
|
|
20
|
+
/* ── Typography ── */
|
|
21
|
+
--password-strength-label-fg-default: var(--a-fg-muted);
|
|
22
|
+
--password-strength-label-size-default: var(--a-ui-sm);
|
|
23
|
+
--password-strength-label-weight-default: var(--a-weight-medium);
|
|
24
|
+
|
|
25
|
+
/* ── Per-score label color (mirrors the lit-segment color so the
|
|
26
|
+
label visually links to the bar fill state). ── */
|
|
27
|
+
--password-strength-label-fg-weak-default: var(--password-strength-color-weak, var(--password-strength-color-weak-default));
|
|
28
|
+
--password-strength-label-fg-fair-default: var(--password-strength-color-fair, var(--password-strength-color-fair-default));
|
|
29
|
+
--password-strength-label-fg-good-default: var(--password-strength-color-good, var(--password-strength-color-good-default));
|
|
30
|
+
--password-strength-label-fg-strong-default: var(--password-strength-color-strong, var(--password-strength-color-strong-default));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ── Host ── */
|
|
34
|
+
:scope {
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: var(--password-strength-stack-gap, var(--password-strength-stack-gap-default));
|
|
39
|
+
width: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── Bar ── */
|
|
43
|
+
[slot="bar"] {
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: repeat(4, 1fr);
|
|
46
|
+
gap: var(--password-strength-gap, var(--password-strength-gap-default));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[slot="segment"] {
|
|
50
|
+
height: var(--password-strength-segment-height, var(--password-strength-segment-height-default));
|
|
51
|
+
background: var(--password-strength-segment-bg, var(--password-strength-segment-bg-default));
|
|
52
|
+
border-radius: var(--password-strength-radius, var(--password-strength-radius-default));
|
|
53
|
+
transition: background var(--a-duration-fast) var(--a-easing);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── Lit segments — color depends on host [data-score] ── */
|
|
57
|
+
:scope[data-score="0"] [slot="segment"][data-lit] { background: var(--password-strength-color-weak, var(--password-strength-color-weak-default)); }
|
|
58
|
+
:scope[data-score="1"] [slot="segment"][data-lit] { background: var(--password-strength-color-fair, var(--password-strength-color-fair-default)); }
|
|
59
|
+
:scope[data-score="2"] [slot="segment"][data-lit] { background: var(--password-strength-color-good, var(--password-strength-color-good-default)); }
|
|
60
|
+
:scope[data-score="3"] [slot="segment"][data-lit] { background: var(--password-strength-color-strong, var(--password-strength-color-strong-default)); }
|
|
61
|
+
|
|
62
|
+
/* ── Label ── */
|
|
63
|
+
[slot="label"] {
|
|
64
|
+
font-size: var(--password-strength-label-size, var(--password-strength-label-size-default));
|
|
65
|
+
font-weight: var(--password-strength-label-weight, var(--password-strength-label-weight-default));
|
|
66
|
+
color: var(--password-strength-label-fg, var(--password-strength-label-fg-default));
|
|
67
|
+
min-height: 1lh; /* prevent layout jump when label appears */
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:scope[data-score="0"] [slot="label"] { color: var(--password-strength-label-fg-weak, var(--password-strength-label-fg-weak-default)); }
|
|
71
|
+
:scope[data-score="1"] [slot="label"] { color: var(--password-strength-label-fg-fair, var(--password-strength-label-fg-fair-default)); }
|
|
72
|
+
:scope[data-score="2"] [slot="label"] { color: var(--password-strength-label-fg-good, var(--password-strength-label-fg-good-default)); }
|
|
73
|
+
:scope[data-score="3"] [slot="label"] { color: var(--password-strength-label-fg-strong, var(--password-strength-label-fg-strong-default)); }
|
|
74
|
+
|
|
75
|
+
/* ── Hide label when [show-label] is false-y. The class.js clears
|
|
76
|
+
the textContent in that case; this provides belt-and-suspenders. ── */
|
|
77
|
+
:scope:not([show-label]) [slot="label"] {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<password-strength-ui>` — Visual strength indicator for password inputs — 4-segment bar (weak /
|
|
3
|
+
fair / good / strong) computed from a heuristic combining length,
|
|
4
|
+
character-class diversity, and repeat-pattern penalty. Pairs with
|
|
5
|
+
`<input-ui type="password">` via JS:
|
|
6
|
+
input.addEventListener('input', e => meter.value = e.target.value)
|
|
7
|
+
Read-only display primitive — no form participation. Emits a
|
|
8
|
+
`score-change` event when the bucket changes so consumers can gate
|
|
9
|
+
a submit button on `detail.satisfied` (score ≥ min-score).
|
|
10
|
+
|
|
11
|
+
**Security note:** [value] is held as a JS property only, NOT
|
|
12
|
+
reflected to a DOM attribute. The element stamps the bar + label
|
|
13
|
+
from the property; the password never appears in the rendered HTML.
|
|
14
|
+
Do not set the value via setAttribute (it will be no-op).
|
|
15
|
+
|
|
16
|
+
*
|
|
17
|
+
* @see https://ui-kit.exe.xyz/site/components/password-strength
|
|
18
|
+
*
|
|
19
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
20
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
21
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
22
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
23
|
+
* needed beyond what the yaml `events:` block can express.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { UIElement } from '../../core/element.js';
|
|
27
|
+
|
|
28
|
+
export interface PasswordStrengthScoreChangeEventDetail {
|
|
29
|
+
label: string;
|
|
30
|
+
satisfied: string;
|
|
31
|
+
score: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PasswordStrengthScoreChangeEvent = CustomEvent<PasswordStrengthScoreChangeEventDetail>;
|
|
35
|
+
|
|
36
|
+
export class UIPasswordStrength extends UIElement {
|
|
37
|
+
/** Minimum acceptable score (0–3). The `score-change` event's
|
|
38
|
+
`detail.satisfied` boolean reflects whether the current score
|
|
39
|
+
meets this threshold. Useful for gating a submit button.
|
|
40
|
+
*/
|
|
41
|
+
minScore: number;
|
|
42
|
+
/** Display the textual score label ("Weak" / "Fair" / "Good" /
|
|
43
|
+
"Strong") below the bar. Defaults to true.
|
|
44
|
+
*/
|
|
45
|
+
showLabel: boolean;
|
|
46
|
+
/** The password string to score. JS-property only — NOT reflected
|
|
47
|
+
to a DOM attribute (so the password never leaks into rendered
|
|
48
|
+
HTML). Wire to an input via `input.addEventListener('input', e =>
|
|
49
|
+
meter.value = e.target.value)`.
|
|
50
|
+
*/
|
|
51
|
+
value: string;
|
|
52
|
+
|
|
53
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
54
|
+
type: K,
|
|
55
|
+
listener: (this: UIPasswordStrength, ev: HTMLElementEventMap[K]) => unknown,
|
|
56
|
+
options?: boolean | AddEventListenerOptions,
|
|
57
|
+
): void;
|
|
58
|
+
addEventListener(type: 'score-change', listener: (ev: PasswordStrengthScoreChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<password-strength-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UIPasswordStrength } from '@adia-ai/web-components/components/password-strength/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UIPasswordStrength } from './password-strength.class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('password-strength-ui', UIPasswordStrength);
|
|
16
|
+
|
|
17
|
+
export { UIPasswordStrength };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
2
|
+
name: UIPasswordStrength
|
|
3
|
+
tag: password-strength-ui
|
|
4
|
+
status: stable
|
|
5
|
+
component: PasswordStrength
|
|
6
|
+
category: display
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Visual strength indicator for password inputs — 4-segment bar (weak /
|
|
10
|
+
fair / good / strong) computed from a heuristic combining length,
|
|
11
|
+
character-class diversity, and repeat-pattern penalty. Pairs with
|
|
12
|
+
`<input-ui type="password">` via JS:
|
|
13
|
+
input.addEventListener('input', e => meter.value = e.target.value)
|
|
14
|
+
Read-only display primitive — no form participation. Emits a
|
|
15
|
+
`score-change` event when the bucket changes so consumers can gate
|
|
16
|
+
a submit button on `detail.satisfied` (score ≥ min-score).
|
|
17
|
+
|
|
18
|
+
**Security note:** [value] is held as a JS property only, NOT
|
|
19
|
+
reflected to a DOM attribute. The element stamps the bar + label
|
|
20
|
+
from the property; the password never appears in the rendered HTML.
|
|
21
|
+
Do not set the value via setAttribute (it will be no-op).
|
|
22
|
+
props:
|
|
23
|
+
value:
|
|
24
|
+
description: |
|
|
25
|
+
The password string to score. JS-property only — NOT reflected
|
|
26
|
+
to a DOM attribute (so the password never leaks into rendered
|
|
27
|
+
HTML). Wire to an input via `input.addEventListener('input', e =>
|
|
28
|
+
meter.value = e.target.value)`.
|
|
29
|
+
type: string
|
|
30
|
+
default: ""
|
|
31
|
+
dynamic: true
|
|
32
|
+
minScore:
|
|
33
|
+
description: |
|
|
34
|
+
Minimum acceptable score (0–3). The `score-change` event's
|
|
35
|
+
`detail.satisfied` boolean reflects whether the current score
|
|
36
|
+
meets this threshold. Useful for gating a submit button.
|
|
37
|
+
type: number
|
|
38
|
+
default: 2
|
|
39
|
+
reflect: true
|
|
40
|
+
attribute: min-score
|
|
41
|
+
showLabel:
|
|
42
|
+
description: |
|
|
43
|
+
Display the textual score label ("Weak" / "Fair" / "Good" /
|
|
44
|
+
"Strong") below the bar. Defaults to true.
|
|
45
|
+
type: boolean
|
|
46
|
+
default: true
|
|
47
|
+
reflect: true
|
|
48
|
+
attribute: show-label
|
|
49
|
+
events:
|
|
50
|
+
score-change:
|
|
51
|
+
description: Fired when the computed score crosses a bucket boundary (not on every keystroke). Detail carries the new score + label + satisfied flag.
|
|
52
|
+
detail:
|
|
53
|
+
score: number
|
|
54
|
+
label: string
|
|
55
|
+
satisfied: boolean
|
|
56
|
+
slots:
|
|
57
|
+
default:
|
|
58
|
+
description: Optional area for requirements checklist content (e.g. <ul> of "at least 8 characters", "mixed case", etc.).
|
|
59
|
+
states:
|
|
60
|
+
- name: idle
|
|
61
|
+
description: Default — no value set, all segments grey.
|
|
62
|
+
- name: scored
|
|
63
|
+
description: Value present; segments lit per score 0..3.
|
|
64
|
+
attribute: data-score
|
|
65
|
+
tokens:
|
|
66
|
+
--password-strength-segment-height:
|
|
67
|
+
description: Height of each bar segment.
|
|
68
|
+
default: 4px
|
|
69
|
+
--password-strength-gap:
|
|
70
|
+
description: Gap between segments.
|
|
71
|
+
default: var(--a-space-1)
|
|
72
|
+
--password-strength-radius:
|
|
73
|
+
description: Segment border radius.
|
|
74
|
+
default: var(--a-radius-full)
|
|
75
|
+
--password-strength-segment-bg:
|
|
76
|
+
description: Unlit segment background (track color).
|
|
77
|
+
default: var(--a-canvas-1-scrim)
|
|
78
|
+
--password-strength-color-weak:
|
|
79
|
+
description: Color for score=0 lit segments.
|
|
80
|
+
default: var(--a-danger-bg)
|
|
81
|
+
--password-strength-color-fair:
|
|
82
|
+
description: Color for score=1 lit segments.
|
|
83
|
+
default: var(--a-warning-bg)
|
|
84
|
+
--password-strength-color-good:
|
|
85
|
+
description: Color for score=2 lit segments.
|
|
86
|
+
default: var(--a-info-bg)
|
|
87
|
+
--password-strength-color-strong:
|
|
88
|
+
description: Color for score=3 lit segments.
|
|
89
|
+
default: var(--a-success-bg)
|
|
90
|
+
--password-strength-label-fg:
|
|
91
|
+
description: Label text color.
|
|
92
|
+
default: var(--a-fg-muted)
|
|
93
|
+
--password-strength-label-size:
|
|
94
|
+
description: Label font size.
|
|
95
|
+
default: var(--a-ui-sm)
|
|
96
|
+
a2ui:
|
|
97
|
+
rules:
|
|
98
|
+
- rule: "Pair with <input-ui type=password> via a JS listener (input.addEventListener('input', e => meter.value = e.target.value)). The meter does NOT participate in form data — it is a display indicator."
|
|
99
|
+
reason: "Pairing contract."
|
|
100
|
+
- rule: "The score is 0 (Weak) / 1 (Fair) / 2 (Good) / 3 (Strong). [min-score] sets the threshold for the `satisfied` boolean in score-change events. Use the boolean to gate a submit button."
|
|
101
|
+
reason: "Score semantics + threshold contract."
|
|
102
|
+
- rule: "Do NOT set value via setAttribute — value is JS-property only and never appears in rendered HTML (security: avoids leaking the password into the DOM)."
|
|
103
|
+
reason: "Security contract."
|
|
104
|
+
anti_patterns:
|
|
105
|
+
- wrong: |
|
|
106
|
+
<password-strength-ui value="hunter2"></password-strength-ui>
|
|
107
|
+
why: |
|
|
108
|
+
`value` is JS-property only; the attribute is ignored. The
|
|
109
|
+
password also shouldn't be authored into HTML at all (server
|
|
110
|
+
template / static page) — that defeats the purpose.
|
|
111
|
+
fix: |
|
|
112
|
+
<password-strength-ui id="m"></password-strength-ui>
|
|
113
|
+
<script>document.getElementById('m').value = pwd;</script>
|
|
114
|
+
examples:
|
|
115
|
+
- name: paired-with-input
|
|
116
|
+
description: Standard pairing with input-ui[type=password] via input listener.
|
|
117
|
+
a2ui: |
|
|
118
|
+
[
|
|
119
|
+
{
|
|
120
|
+
"id": "field",
|
|
121
|
+
"component": "Field",
|
|
122
|
+
"label": "Password",
|
|
123
|
+
"children": ["pwd", "meter"]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "pwd",
|
|
127
|
+
"component": "Input",
|
|
128
|
+
"type": "password",
|
|
129
|
+
"name": "password"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "meter",
|
|
133
|
+
"component": "PasswordStrength"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
keywords:
|
|
137
|
+
- password-strength
|
|
138
|
+
- strength-meter
|
|
139
|
+
- password
|
|
140
|
+
- strength
|
|
141
|
+
- meter
|
|
142
|
+
- security
|
|
143
|
+
synonyms:
|
|
144
|
+
password:
|
|
145
|
+
- password-strength
|
|
146
|
+
- strength-meter
|
|
147
|
+
strength:
|
|
148
|
+
- password-strength
|
|
149
|
+
- meter
|
|
150
|
+
related:
|
|
151
|
+
- input
|
|
152
|
+
- field
|
|
153
|
+
- progress
|
|
@@ -5,15 +5,11 @@
|
|
|
5
5
|
--popover-py-default: var(--a-space-2);
|
|
6
6
|
--popover-radius-default: var(--a-radius-lg);
|
|
7
7
|
|
|
8
|
-
/* ── Colors
|
|
8
|
+
/* ── Colors (default panel chrome — opt out per `:has(>card-ui)` rule
|
|
9
|
+
below or by setting tokens to transparent / 0) ── */
|
|
9
10
|
--popover-bg-default: var(--a-bg-subtle);
|
|
10
11
|
--popover-border-default: var(--a-border-subtle);
|
|
11
12
|
--popover-shadow-default: var(--a-shadow-lg);
|
|
12
|
-
--popover-fg-default: var(--a-fg);
|
|
13
|
-
|
|
14
|
-
/* ── Typography ── */
|
|
15
|
-
--popover-font-default: var(--a-font-family);
|
|
16
|
-
--popover-font-size-default: var(--a-ui-size);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
:scope {
|
|
@@ -34,25 +30,55 @@
|
|
|
34
30
|
display: none !important;
|
|
35
31
|
}
|
|
36
32
|
|
|
33
|
+
/* ── popover-ui's CSS opinions, narrowly scoped ──
|
|
34
|
+
*
|
|
35
|
+
* What's HERE and why:
|
|
36
|
+
* • [slot="content"]:not(:popover-open) display:none — required Popover
|
|
37
|
+
* API mechanics (closed-state visibility for slotted children).
|
|
38
|
+
* • [slot="content"] margin: 0 — reset UA stylesheet's `margin: auto`
|
|
39
|
+
* on popover elements (otherwise popover centers in viewport).
|
|
40
|
+
* • [slot="content"] box-sizing — universal sanity.
|
|
41
|
+
* • Default panel chrome — defensive for the bare-HTML case
|
|
42
|
+
* (<div slot="content">Hello</div> needs to be visible against the
|
|
43
|
+
* page). Opt out by putting a surface primitive (card-ui / drawer-ui
|
|
44
|
+
* / menu-ui) as the only child — they own their own chrome.
|
|
45
|
+
* • Light enter/exit animation (opacity + 4px translate) tied to the
|
|
46
|
+
* :popover-open state; honors prefers-reduced-motion.
|
|
47
|
+
*
|
|
48
|
+
* What's deliberately NOT here:
|
|
49
|
+
* • width / max-width / max-height / overflow-y — set by core/anchor.js
|
|
50
|
+
* inline during anchoring; those are the anchor module's contract,
|
|
51
|
+
* not the shell's.
|
|
52
|
+
* • font-family / font-size / color — inherit from the page naturally.
|
|
53
|
+
* • Prose first/last-child margin resets — touched consumer DOM.
|
|
54
|
+
*/
|
|
37
55
|
[slot="content"] {
|
|
38
56
|
box-sizing: border-box;
|
|
57
|
+
/* Reset UA stylesheet's `[popover]` defaults — it sets `margin: auto`
|
|
58
|
+
(centers in viewport) AND `padding: 0.25em` (~3.75 px at default
|
|
59
|
+
font-size, leaks into card-ui / drawer-ui consumers and looks like
|
|
60
|
+
phantom inset between popover edge and inner surface). Both must
|
|
61
|
+
be zeroed; the chrome rule below re-applies padding only when no
|
|
62
|
+
surface primitive is in the slot. */
|
|
39
63
|
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
opacity: 1;
|
|
66
|
+
translate: 0 0;
|
|
67
|
+
transition: opacity var(--a-duration-fast) var(--a-easing-out),
|
|
68
|
+
translate var(--a-duration-fast) var(--a-easing-out);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Skip chrome when the slot IS a surface primitive itself
|
|
72
|
+
(`<card-ui slot="content">…`) AND when it CONTAINS one as the only
|
|
73
|
+
child (`<div slot="content"><card-ui>…</card-ui></div>`). Both
|
|
74
|
+
shapes appear in consumer code; both should let the inner surface
|
|
75
|
+
own padding + bg + border + radius + shadow. */
|
|
76
|
+
[slot="content"]:not(card-ui):not(drawer-ui):not(menu-ui):not(:has(> card-ui:only-child)):not(:has(> drawer-ui:only-child)):not(:has(> menu-ui:only-child)) {
|
|
40
77
|
padding: var(--popover-py, var(--popover-py-default)) var(--popover-px, var(--popover-px-default));
|
|
41
78
|
background: var(--popover-bg, var(--popover-bg-default));
|
|
42
79
|
border: 1px solid var(--popover-border, var(--popover-border-default));
|
|
43
80
|
border-radius: var(--popover-radius, var(--popover-radius-default));
|
|
44
81
|
box-shadow: var(--popover-shadow, var(--popover-shadow-default));
|
|
45
|
-
font-family: var(--popover-font, var(--popover-font-default));
|
|
46
|
-
font-size: var(--popover-font-size, var(--popover-font-size-default));
|
|
47
|
-
color: var(--popover-fg, var(--popover-fg-default));
|
|
48
|
-
max-height: calc(100vh - 3rem);
|
|
49
|
-
overflow-y: auto;
|
|
50
|
-
/* Fade + lift in on first paint. @starting-style is the initial frame
|
|
51
|
-
browsers paint before the popover transitions to its open state. */
|
|
52
|
-
opacity: 1;
|
|
53
|
-
translate: 0 0;
|
|
54
|
-
transition: opacity var(--a-duration-fast) var(--a-easing-out),
|
|
55
|
-
translate var(--a-duration-fast) var(--a-easing-out);
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
[slot="content"]:popover-open {
|
|
@@ -65,10 +91,4 @@
|
|
|
65
91
|
@media (prefers-reduced-motion: reduce) {
|
|
66
92
|
[slot="content"] { transition: none; }
|
|
67
93
|
}
|
|
68
|
-
|
|
69
|
-
/* Collapse default margins on the first/last block child so the
|
|
70
|
-
content sits snug inside --popover-py (no phantom extra space
|
|
71
|
-
below <p>, <h*>, etc.). */
|
|
72
|
-
[slot="content"] > :first-child { margin-block-start: 0; }
|
|
73
|
-
[slot="content"] > :last-child { margin-block-end: 0; }
|
|
74
94
|
}
|
|
@@ -76,10 +76,14 @@ a2ui:
|
|
|
76
76
|
everything else (inline forms, color pickers, theme panels,
|
|
77
77
|
export menus with non-action content).
|
|
78
78
|
- >-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
79
|
+
Placement convention (ADR-0034): default `bottom` centers under
|
|
80
|
+
the trigger — correct for wide pickers (calendar, color,
|
|
81
|
+
date-range, filter forms). Use `bottom-start` for trigger-width
|
|
82
|
+
menus (action lists, listboxes, breadcrumb overflow — popover
|
|
83
|
+
width ≈ trigger width). Use `bottom-end` only when the trigger
|
|
84
|
+
sits at the right edge of a container by construction (toolbar
|
|
85
|
+
spillover). Use `top-*` when the trigger sits low in the
|
|
86
|
+
viewport (statusbar). [gap] (default 4px) sets offset from anchor.
|
|
83
87
|
- >-
|
|
84
88
|
[trigger="hover"] is for non-essential disclosure only — never
|
|
85
89
|
use it for popovers containing inputs, destructive actions, or
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330" width="330" height="330" shape-rendering="crispEdges">
|
|
2
|
+
<rect width="100%" height="100%" fill="#fff"/>
|
|
3
|
+
<g fill="#000"><rect x="40" y="40" width="10" height="10"/><rect x="50" y="40" width="10" height="10"/><rect x="60" y="40" width="10" height="10"/><rect x="70" y="40" width="10" height="10"/><rect x="80" y="40" width="10" height="10"/><rect x="90" y="40" width="10" height="10"/><rect x="100" y="40" width="10" height="10"/><rect x="120" y="40" width="10" height="10"/><rect x="150" y="40" width="10" height="10"/><rect x="170" y="40" width="10" height="10"/><rect x="180" y="40" width="10" height="10"/><rect x="190" y="40" width="10" height="10"/><rect x="220" y="40" width="10" height="10"/><rect x="230" y="40" width="10" height="10"/><rect x="240" y="40" width="10" height="10"/><rect x="250" y="40" width="10" height="10"/><rect x="260" y="40" width="10" height="10"/><rect x="270" y="40" width="10" height="10"/><rect x="280" y="40" width="10" height="10"/><rect x="40" y="50" width="10" height="10"/><rect x="100" y="50" width="10" height="10"/><rect x="190" y="50" width="10" height="10"/><rect x="220" y="50" width="10" height="10"/><rect x="280" y="50" width="10" height="10"/><rect x="40" y="60" width="10" height="10"/><rect x="60" y="60" width="10" height="10"/><rect x="70" y="60" width="10" height="10"/><rect x="80" y="60" width="10" height="10"/><rect x="100" y="60" width="10" height="10"/><rect x="120" y="60" width="10" height="10"/><rect x="140" y="60" width="10" height="10"/><rect x="160" y="60" width="10" height="10"/><rect x="170" y="60" width="10" height="10"/><rect x="220" y="60" width="10" height="10"/><rect x="240" y="60" width="10" height="10"/><rect x="250" y="60" width="10" height="10"/><rect x="260" y="60" width="10" height="10"/><rect x="280" y="60" width="10" height="10"/><rect x="40" y="70" width="10" height="10"/><rect x="60" y="70" width="10" height="10"/><rect x="70" y="70" width="10" height="10"/><rect x="80" y="70" width="10" height="10"/><rect x="100" y="70" width="10" height="10"/><rect x="130" y="70" width="10" height="10"/><rect x="150" y="70" width="10" height="10"/><rect x="160" y="70" width="10" height="10"/><rect x="190" y="70" width="10" height="10"/><rect x="220" y="70" width="10" height="10"/><rect x="240" y="70" width="10" height="10"/><rect x="250" y="70" width="10" height="10"/><rect x="260" y="70" width="10" height="10"/><rect x="280" y="70" width="10" height="10"/><rect x="40" y="80" width="10" height="10"/><rect x="60" y="80" width="10" height="10"/><rect x="70" y="80" width="10" height="10"/><rect x="80" y="80" width="10" height="10"/><rect x="100" y="80" width="10" height="10"/><rect x="130" y="80" width="10" height="10"/><rect x="190" y="80" width="10" height="10"/><rect x="200" y="80" width="10" height="10"/><rect x="220" y="80" width="10" height="10"/><rect x="240" y="80" width="10" height="10"/><rect x="250" y="80" width="10" height="10"/><rect x="260" y="80" width="10" height="10"/><rect x="280" y="80" width="10" height="10"/><rect x="40" y="90" width="10" height="10"/><rect x="100" y="90" width="10" height="10"/><rect x="120" y="90" width="10" height="10"/><rect x="160" y="90" width="10" height="10"/><rect x="170" y="90" width="10" height="10"/><rect x="180" y="90" width="10" height="10"/><rect x="220" y="90" width="10" height="10"/><rect x="280" y="90" width="10" height="10"/><rect x="40" y="100" width="10" height="10"/><rect x="50" y="100" width="10" height="10"/><rect x="60" y="100" width="10" height="10"/><rect x="70" y="100" width="10" height="10"/><rect x="80" y="100" width="10" height="10"/><rect x="90" y="100" width="10" height="10"/><rect x="100" y="100" width="10" height="10"/><rect x="120" y="100" width="10" height="10"/><rect x="140" y="100" width="10" height="10"/><rect x="160" y="100" width="10" height="10"/><rect x="180" y="100" width="10" height="10"/><rect x="200" y="100" width="10" height="10"/><rect x="220" y="100" width="10" height="10"/><rect x="230" y="100" width="10" height="10"/><rect x="240" y="100" width="10" height="10"/><rect x="250" y="100" width="10" height="10"/><rect x="260" y="100" width="10" height="10"/><rect x="270" y="100" width="10" height="10"/><rect x="280" y="100" width="10" height="10"/><rect x="160" y="110" width="10" height="10"/><rect x="170" y="110" width="10" height="10"/><rect x="190" y="110" width="10" height="10"/><rect x="200" y="110" width="10" height="10"/><rect x="40" y="120" width="10" height="10"/><rect x="60" y="120" width="10" height="10"/><rect x="100" y="120" width="10" height="10"/><rect x="110" y="120" width="10" height="10"/><rect x="130" y="120" width="10" height="10"/><rect x="140" y="120" width="10" height="10"/><rect x="150" y="120" width="10" height="10"/><rect x="160" y="120" width="10" height="10"/><rect x="170" y="120" width="10" height="10"/><rect x="190" y="120" width="10" height="10"/><rect x="230" y="120" width="10" height="10"/><rect x="260" y="120" width="10" height="10"/><rect x="280" y="120" width="10" height="10"/><rect x="40" y="130" width="10" height="10"/><rect x="50" y="130" width="10" height="10"/><rect x="60" y="130" width="10" height="10"/><rect x="70" y="130" width="10" height="10"/><rect x="130" y="130" width="10" height="10"/><rect x="150" y="130" width="10" height="10"/><rect x="160" y="130" width="10" height="10"/><rect x="190" y="130" width="10" height="10"/><rect x="200" y="130" width="10" height="10"/><rect x="220" y="130" width="10" height="10"/><rect x="230" y="130" width="10" height="10"/><rect x="250" y="130" width="10" height="10"/><rect x="270" y="130" width="10" height="10"/><rect x="280" y="130" width="10" height="10"/><rect x="60" y="140" width="10" height="10"/><rect x="70" y="140" width="10" height="10"/><rect x="80" y="140" width="10" height="10"/><rect x="100" y="140" width="10" height="10"/><rect x="110" y="140" width="10" height="10"/><rect x="130" y="140" width="10" height="10"/><rect x="140" y="140" width="10" height="10"/><rect x="170" y="140" width="10" height="10"/><rect x="180" y="140" width="10" height="10"/><rect x="190" y="140" width="10" height="10"/><rect x="220" y="140" width="10" height="10"/><rect x="240" y="140" width="10" height="10"/><rect x="250" y="140" width="10" height="10"/><rect x="260" y="140" width="10" height="10"/><rect x="280" y="140" width="10" height="10"/><rect x="70" y="150" width="10" height="10"/><rect x="80" y="150" width="10" height="10"/><rect x="110" y="150" width="10" height="10"/><rect x="160" y="150" width="10" height="10"/><rect x="180" y="150" width="10" height="10"/><rect x="200" y="150" width="10" height="10"/><rect x="250" y="150" width="10" height="10"/><rect x="40" y="160" width="10" height="10"/><rect x="70" y="160" width="10" height="10"/><rect x="90" y="160" width="10" height="10"/><rect x="100" y="160" width="10" height="10"/><rect x="130" y="160" width="10" height="10"/><rect x="150" y="160" width="10" height="10"/><rect x="170" y="160" width="10" height="10"/><rect x="180" y="160" width="10" height="10"/><rect x="190" y="160" width="10" height="10"/><rect x="200" y="160" width="10" height="10"/><rect x="220" y="160" width="10" height="10"/><rect x="230" y="160" width="10" height="10"/><rect x="280" y="160" width="10" height="10"/><rect x="70" y="170" width="10" height="10"/><rect x="80" y="170" width="10" height="10"/><rect x="90" y="170" width="10" height="10"/><rect x="110" y="170" width="10" height="10"/><rect x="120" y="170" width="10" height="10"/><rect x="170" y="170" width="10" height="10"/><rect x="180" y="170" width="10" height="10"/><rect x="190" y="170" width="10" height="10"/><rect x="200" y="170" width="10" height="10"/><rect x="220" y="170" width="10" height="10"/><rect x="230" y="170" width="10" height="10"/><rect x="270" y="170" width="10" height="10"/><rect x="280" y="170" width="10" height="10"/><rect x="40" y="180" width="10" height="10"/><rect x="50" y="180" width="10" height="10"/><rect x="60" y="180" width="10" height="10"/><rect x="90" y="180" width="10" height="10"/><rect x="100" y="180" width="10" height="10"/><rect x="110" y="180" width="10" height="10"/><rect x="130" y="180" width="10" height="10"/><rect x="150" y="180" width="10" height="10"/><rect x="190" y="180" width="10" height="10"/><rect x="200" y="180" width="10" height="10"/><rect x="210" y="180" width="10" height="10"/><rect x="250" y="180" width="10" height="10"/><rect x="260" y="180" width="10" height="10"/><rect x="280" y="180" width="10" height="10"/><rect x="60" y="190" width="10" height="10"/><rect x="80" y="190" width="10" height="10"/><rect x="90" y="190" width="10" height="10"/><rect x="120" y="190" width="10" height="10"/><rect x="130" y="190" width="10" height="10"/><rect x="140" y="190" width="10" height="10"/><rect x="170" y="190" width="10" height="10"/><rect x="200" y="190" width="10" height="10"/><rect x="210" y="190" width="10" height="10"/><rect x="230" y="190" width="10" height="10"/><rect x="240" y="190" width="10" height="10"/><rect x="250" y="190" width="10" height="10"/><rect x="40" y="200" width="10" height="10"/><rect x="50" y="200" width="10" height="10"/><rect x="70" y="200" width="10" height="10"/><rect x="90" y="200" width="10" height="10"/><rect x="100" y="200" width="10" height="10"/><rect x="110" y="200" width="10" height="10"/><rect x="120" y="200" width="10" height="10"/><rect x="150" y="200" width="10" height="10"/><rect x="160" y="200" width="10" height="10"/><rect x="170" y="200" width="10" height="10"/><rect x="190" y="200" width="10" height="10"/><rect x="200" y="200" width="10" height="10"/><rect x="210" y="200" width="10" height="10"/><rect x="220" y="200" width="10" height="10"/><rect x="230" y="200" width="10" height="10"/><rect x="240" y="200" width="10" height="10"/><rect x="270" y="200" width="10" height="10"/><rect x="120" y="210" width="10" height="10"/><rect x="130" y="210" width="10" height="10"/><rect x="140" y="210" width="10" height="10"/><rect x="160" y="210" width="10" height="10"/><rect x="200" y="210" width="10" height="10"/><rect x="240" y="210" width="10" height="10"/><rect x="280" y="210" width="10" height="10"/><rect x="40" y="220" width="10" height="10"/><rect x="50" y="220" width="10" height="10"/><rect x="60" y="220" width="10" height="10"/><rect x="70" y="220" width="10" height="10"/><rect x="80" y="220" width="10" height="10"/><rect x="90" y="220" width="10" height="10"/><rect x="100" y="220" width="10" height="10"/><rect x="120" y="220" width="10" height="10"/><rect x="160" y="220" width="10" height="10"/><rect x="170" y="220" width="10" height="10"/><rect x="180" y="220" width="10" height="10"/><rect x="200" y="220" width="10" height="10"/><rect x="220" y="220" width="10" height="10"/><rect x="240" y="220" width="10" height="10"/><rect x="280" y="220" width="10" height="10"/><rect x="40" y="230" width="10" height="10"/><rect x="100" y="230" width="10" height="10"/><rect x="130" y="230" width="10" height="10"/><rect x="140" y="230" width="10" height="10"/><rect x="150" y="230" width="10" height="10"/><rect x="160" y="230" width="10" height="10"/><rect x="180" y="230" width="10" height="10"/><rect x="190" y="230" width="10" height="10"/><rect x="200" y="230" width="10" height="10"/><rect x="240" y="230" width="10" height="10"/><rect x="40" y="240" width="10" height="10"/><rect x="60" y="240" width="10" height="10"/><rect x="70" y="240" width="10" height="10"/><rect x="80" y="240" width="10" height="10"/><rect x="100" y="240" width="10" height="10"/><rect x="130" y="240" width="10" height="10"/><rect x="170" y="240" width="10" height="10"/><rect x="180" y="240" width="10" height="10"/><rect x="190" y="240" width="10" height="10"/><rect x="200" y="240" width="10" height="10"/><rect x="210" y="240" width="10" height="10"/><rect x="220" y="240" width="10" height="10"/><rect x="230" y="240" width="10" height="10"/><rect x="240" y="240" width="10" height="10"/><rect x="270" y="240" width="10" height="10"/><rect x="280" y="240" width="10" height="10"/><rect x="40" y="250" width="10" height="10"/><rect x="60" y="250" width="10" height="10"/><rect x="70" y="250" width="10" height="10"/><rect x="80" y="250" width="10" height="10"/><rect x="100" y="250" width="10" height="10"/><rect x="140" y="250" width="10" height="10"/><rect x="170" y="250" width="10" height="10"/><rect x="180" y="250" width="10" height="10"/><rect x="200" y="250" width="10" height="10"/><rect x="210" y="250" width="10" height="10"/><rect x="240" y="250" width="10" height="10"/><rect x="260" y="250" width="10" height="10"/><rect x="270" y="250" width="10" height="10"/><rect x="40" y="260" width="10" height="10"/><rect x="60" y="260" width="10" height="10"/><rect x="70" y="260" width="10" height="10"/><rect x="80" y="260" width="10" height="10"/><rect x="100" y="260" width="10" height="10"/><rect x="120" y="260" width="10" height="10"/><rect x="150" y="260" width="10" height="10"/><rect x="210" y="260" width="10" height="10"/><rect x="230" y="260" width="10" height="10"/><rect x="240" y="260" width="10" height="10"/><rect x="250" y="260" width="10" height="10"/><rect x="270" y="260" width="10" height="10"/><rect x="280" y="260" width="10" height="10"/><rect x="40" y="270" width="10" height="10"/><rect x="100" y="270" width="10" height="10"/><rect x="130" y="270" width="10" height="10"/><rect x="170" y="270" width="10" height="10"/><rect x="200" y="270" width="10" height="10"/><rect x="230" y="270" width="10" height="10"/><rect x="240" y="270" width="10" height="10"/><rect x="40" y="280" width="10" height="10"/><rect x="50" y="280" width="10" height="10"/><rect x="60" y="280" width="10" height="10"/><rect x="70" y="280" width="10" height="10"/><rect x="80" y="280" width="10" height="10"/><rect x="90" y="280" width="10" height="10"/><rect x="100" y="280" width="10" height="10"/><rect x="120" y="280" width="10" height="10"/><rect x="130" y="280" width="10" height="10"/><rect x="150" y="280" width="10" height="10"/><rect x="160" y="280" width="10" height="10"/><rect x="170" y="280" width="10" height="10"/><rect x="190" y="280" width="10" height="10"/><rect x="200" y="280" width="10" height="10"/><rect x="220" y="280" width="10" height="10"/><rect x="250" y="280" width="10" height="10"/><rect x="280" y="280" width="10" height="10"/></g>
|
|
4
|
+
</svg>
|