@adia-ai/web-components 0.6.35 → 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 +56 -0
- 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/combobox/combobox.css +12 -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 +3 -1
- package/components/date-range-picker/date-range-picker.css +4 -1
- 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.css +7 -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/input/input.css +15 -1
- package/components/input/input.test.js +40 -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/search/search.class.js +2 -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 +4 -1
- package/components/tag/tag.a2ui.json +10 -0
- package/components/tag/tag.class.js +8 -1
- package/components/tag/tag.css +108 -20
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +99 -1
- package/components/tag/tag.yaml +20 -0
- package/components/tags-input/tags-input.class.js +10 -3
- package/components/tags-input/tags-input.css +12 -3
- package/components/textarea/textarea.css +10 -1
- 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/core/provider.js +19 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +101 -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,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/PasswordStrength.json",
|
|
4
|
+
"title": "PasswordStrength",
|
|
5
|
+
"description": "Visual strength indicator for password inputs — 4-segment bar (weak /\nfair / good / strong) computed from a heuristic combining length,\ncharacter-class diversity, and repeat-pattern penalty. Pairs with\n`<input-ui type=\"password\">` via JS:\n input.addEventListener('input', e => meter.value = e.target.value)\nRead-only display primitive — no form participation. Emits a\n`score-change` event when the bucket changes so consumers can gate\na submit button on `detail.satisfied` (score ≥ min-score).\n\n**Security note:** [value] is held as a JS property only, NOT\nreflected to a DOM attribute. The element stamps the bar + label\nfrom the property; the password never appears in the rendered HTML.\nDo not set the value via setAttribute (it will be no-op).\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "PasswordStrength"
|
|
18
|
+
},
|
|
19
|
+
"minScore": {
|
|
20
|
+
"description": "Minimum acceptable score (0–3). The `score-change` event's\n`detail.satisfied` boolean reflects whether the current score\nmeets this threshold. Useful for gating a submit button.\n",
|
|
21
|
+
"type": "number",
|
|
22
|
+
"default": 2
|
|
23
|
+
},
|
|
24
|
+
"showLabel": {
|
|
25
|
+
"description": "Display the textual score label (\"Weak\" / \"Fair\" / \"Good\" /\n\"Strong\") below the bar. Defaults to true.\n",
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"default": true
|
|
28
|
+
},
|
|
29
|
+
"value": {
|
|
30
|
+
"description": "The password string to score. JS-property only — NOT reflected\nto a DOM attribute (so the password never leaks into rendered\nHTML). Wire to an input via `input.addEventListener('input', e =>\nmeter.value = e.target.value)`.\n",
|
|
31
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": [
|
|
35
|
+
"component"
|
|
36
|
+
],
|
|
37
|
+
"unevaluatedProperties": false,
|
|
38
|
+
"x-adiaui": {
|
|
39
|
+
"anti_patterns": [
|
|
40
|
+
{
|
|
41
|
+
"fix": "<password-strength-ui id=\"m\"></password-strength-ui>\n<script>document.getElementById('m').value = pwd;</script>\n",
|
|
42
|
+
"why": "`value` is JS-property only; the attribute is ignored. The\npassword also shouldn't be authored into HTML at all (server\ntemplate / static page) — that defeats the purpose.\n",
|
|
43
|
+
"wrong": "<password-strength-ui value=\"hunter2\"></password-strength-ui>\n"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"category": "display",
|
|
47
|
+
"composes": [],
|
|
48
|
+
"events": {
|
|
49
|
+
"score-change": {
|
|
50
|
+
"description": "Fired when the computed score crosses a bucket boundary (not on every keystroke). Detail carries the new score + label + satisfied flag.",
|
|
51
|
+
"detail": {
|
|
52
|
+
"label": "string",
|
|
53
|
+
"satisfied": "boolean",
|
|
54
|
+
"score": "number"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"examples": [
|
|
59
|
+
{
|
|
60
|
+
"description": "Standard pairing with input-ui[type=password] via input listener.",
|
|
61
|
+
"a2ui": "[\n {\n \"id\": \"field\",\n \"component\": \"Field\",\n \"label\": \"Password\",\n \"children\": [\"pwd\", \"meter\"]\n },\n {\n \"id\": \"pwd\",\n \"component\": \"Input\",\n \"type\": \"password\",\n \"name\": \"password\"\n },\n {\n \"id\": \"meter\",\n \"component\": \"PasswordStrength\"\n }\n]\n",
|
|
62
|
+
"name": "paired-with-input"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"keywords": [
|
|
66
|
+
"password-strength",
|
|
67
|
+
"strength-meter",
|
|
68
|
+
"password",
|
|
69
|
+
"strength",
|
|
70
|
+
"meter",
|
|
71
|
+
"security"
|
|
72
|
+
],
|
|
73
|
+
"name": "UIPasswordStrength",
|
|
74
|
+
"related": [
|
|
75
|
+
"input",
|
|
76
|
+
"field",
|
|
77
|
+
"progress"
|
|
78
|
+
],
|
|
79
|
+
"slots": {
|
|
80
|
+
"default": {
|
|
81
|
+
"description": "Optional area for requirements checklist content (e.g. <ul> of \"at least 8 characters\", \"mixed case\", etc.)."
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"states": [
|
|
85
|
+
{
|
|
86
|
+
"description": "Default — no value set, all segments grey.",
|
|
87
|
+
"name": "idle"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"description": "Value present; segments lit per score 0..3.",
|
|
91
|
+
"attribute": "data-score",
|
|
92
|
+
"name": "scored"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"status": "stable",
|
|
96
|
+
"synonyms": {
|
|
97
|
+
"password": [
|
|
98
|
+
"password-strength",
|
|
99
|
+
"strength-meter"
|
|
100
|
+
],
|
|
101
|
+
"strength": [
|
|
102
|
+
"password-strength",
|
|
103
|
+
"meter"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"tag": "password-strength-ui",
|
|
107
|
+
"tokens": {
|
|
108
|
+
"--password-strength-color-fair": {
|
|
109
|
+
"description": "Color for score=1 lit segments.",
|
|
110
|
+
"default": "var(--a-warning-bg)"
|
|
111
|
+
},
|
|
112
|
+
"--password-strength-color-good": {
|
|
113
|
+
"description": "Color for score=2 lit segments.",
|
|
114
|
+
"default": "var(--a-info-bg)"
|
|
115
|
+
},
|
|
116
|
+
"--password-strength-color-strong": {
|
|
117
|
+
"description": "Color for score=3 lit segments.",
|
|
118
|
+
"default": "var(--a-success-bg)"
|
|
119
|
+
},
|
|
120
|
+
"--password-strength-color-weak": {
|
|
121
|
+
"description": "Color for score=0 lit segments.",
|
|
122
|
+
"default": "var(--a-danger-bg)"
|
|
123
|
+
},
|
|
124
|
+
"--password-strength-gap": {
|
|
125
|
+
"description": "Gap between segments.",
|
|
126
|
+
"default": "var(--a-space-1)"
|
|
127
|
+
},
|
|
128
|
+
"--password-strength-label-fg": {
|
|
129
|
+
"description": "Label text color.",
|
|
130
|
+
"default": "var(--a-fg-muted)"
|
|
131
|
+
},
|
|
132
|
+
"--password-strength-label-size": {
|
|
133
|
+
"description": "Label font size.",
|
|
134
|
+
"default": "var(--a-ui-sm)"
|
|
135
|
+
},
|
|
136
|
+
"--password-strength-radius": {
|
|
137
|
+
"description": "Segment border radius.",
|
|
138
|
+
"default": "var(--a-radius-full)"
|
|
139
|
+
},
|
|
140
|
+
"--password-strength-segment-bg": {
|
|
141
|
+
"description": "Unlit segment background (track color).",
|
|
142
|
+
"default": "var(--a-canvas-1-scrim)"
|
|
143
|
+
},
|
|
144
|
+
"--password-strength-segment-height": {
|
|
145
|
+
"description": "Height of each bar segment.",
|
|
146
|
+
"default": "4px"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"traits": [],
|
|
150
|
+
"version": 1
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -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
|