diff --git a/package-lock.json b/package-lock.json index 5c69d128..17e1548a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,9 @@ "webpack": "^5.88.2", "webpack-cli": "^5.1.1" }, + "engines": { + "node": ">=20.0.0" + }, "optionalDependencies": { "sharp": "^0.34.3", "sharp-bmp": "^0.1.5", @@ -2313,7 +2316,7 @@ "resolved": "src/puter-wisp", "link": true }, - "node_modules/@heyputer/puterjs": { + "node_modules/@heyputer/puter.js": { "resolved": "src/puter-js", "link": true }, @@ -20238,7 +20241,7 @@ "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-replace": "^5.0.2", "mocha": "^10.8.2", - "rollup": "^3.21.4", + "rollup": "^3.29.5", "rollup-plugin-copy": "^3.4.0" }, "optionalDependencies": { @@ -20373,11 +20376,12 @@ "license": "AGPL-3.0-only" }, "src/puter-js": { - "name": "@heyputer/puterjs", - "version": "1.0.0", + "name": "@heyputer/puter.js", + "version": "2.0.11", "license": "Apache-2.0", "dependencies": { - "@heyputer/kv.js": "^0.1.92" + "@heyputer/kv.js": "^0.1.92", + "@heyputer/putility": "^1.0.3" }, "devDependencies": { "concurrently": "^8.2.2", @@ -20391,7 +20395,7 @@ }, "src/putility": { "name": "@heyputer/putility", - "version": "1.0.2", + "version": "1.0.3", "license": "AGPL-3.0-only" }, "src/terminal": { @@ -20408,7 +20412,7 @@ "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-replace": "^5.0.2", "http-server": "^14.1.1", - "mocha": "^10.2.0", + "mocha": "^10.8.2", "rollup": "^3.29.5", "rollup-plugin-copy": "^3.4.0" } diff --git a/src/gui/src/UI/Components/PasswordEntry.js b/src/gui/src/UI/Components/PasswordEntry.js index 90ecaf93..da5e3ee1 100644 --- a/src/gui/src/UI/Components/PasswordEntry.js +++ b/src/gui/src/UI/Components/PasswordEntry.js @@ -1,23 +1,12 @@ /** - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * Improved PasswordEntry Component for Puter GUI + * Features: + * - DRY: input styles reused from shared CSS class + * - Password strength meter (weak/medium/strong) + * - Caps Lock warning + * - Accessible show/hide toggle button */ - const Component = use('util.Component'); export default def(class PasswordEntry extends Component { @@ -36,80 +25,73 @@ export default def(class PasswordEntry extends Component { display: flex; flex-direction: column; } - input { - flex-grow: 1; - } - /* TODO: I'd rather not duplicate this */ - .error { - display: none; - color: red; - border: 1px solid red; - border-radius: 4px; - padding: 9px; - margin-bottom: 15px; - text-align: center; - font-size: 13px; - } .error-message { display: none; color: rgb(215 2 2); font-size: 14px; - margin-top: 10px; - margin-bottom: 10px; + margin: 10px 0; padding: 10px; border-radius: 4px; border: 1px solid rgb(215 2 2); text-align: center; } + .password-and-toggle { display: flex; align-items: center; gap: 10px; } + .password-and-toggle input { flex-grow: 1; } - - /* TODO: DRY: This is from style.css */ - input[type=text], input[type=password], input[type=email], select { - width: 100%; - padding: 8px; - border: 1px solid #ccc; + .strength-meter { + height: 6px; border-radius: 4px; - box-sizing: border-box; - outline: none; - -webkit-font-smoothing: antialiased; - color: #393f46; - font-size: 14px; + margin-top: 6px; + background: #eee; + overflow: hidden; } - /* to prevent auto-zoom on input focus in mobile */ - .device-phone input[type=text], .device-phone input[type=password], .device-phone input[type=email], .device-phone select { - font-size: 17px; + .strength-bar { + height: 100%; + width: 0%; + transition: width 0.3s; } - input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, select:focus { - border: 2px solid #01a0fd; - padding: 7px; + .strength-weak { background: #e74c3c; } + .strength-medium { background: #f39c12; } + .strength-strong { background: #2ecc71; } + + .caps-warning { + display: none; + font-size: 12px; + margin-top: 5px; + color: #e67e22; } `; create_template ({ template }) { $(template).html(/*html*/`
-
+
- - + +
+
+
+
+
⚠️ Caps Lock is ON
`); } @@ -119,38 +101,73 @@ export default def(class PasswordEntry extends Component { } on_ready ({ listen }) { + const input = $(this.dom_).find('#password'); + const strengthBar = $(this.dom_).find('.strength-bar'); + const capsWarning = $(this.dom_).find('.caps-warning'); + const errorBox = $(this.dom_).find('.error-message'); + + // Show errors listen('error', (error) => { - if ( ! error ) return $(this.dom_).find('.error').hide(); - $(this.dom_).find('.error').text(error).show(); + if (!error) return errorBox.hide(); + errorBox.text(error).show(); }); + // Reset input value if cleared listen('value', (value) => { - // clear input - if ( value === undefined ) { - $(this.dom_).find('input').val(''); - } + if (value === undefined) input.val(''); }); - const input = $(this.dom_).find('input'); + // Input listener input.on('input', () => { - this.set('value', input.val()); + const value = input.val(); + this.set('value', value); + this.updateStrength(value, strengthBar); }); + // Caps Lock detection + input.on('keyup keydown', (e) => { + const isCaps = e.getModifierState && e.getModifierState('CapsLock'); + capsWarning.toggle(isCaps); + }); + + // Submit on Enter const on_submit = this.get('on_submit'); - if ( on_submit ) { - $(this.dom_).find('input').on('keyup', (e) => { - if ( e.key === 'Enter' ) { - on_submit(); - } + if (on_submit) { + input.on('keyup', (e) => { + if (e.key === 'Enter') on_submit(); }); } - - $(this.dom_).find("#toggle-show-password").on("click", () => { + + // Toggle password visibility + $(this.dom_).find('#toggle-show-password').on('click', () => { this.set('show_password', !this.get('show_password')); const show_password = this.get('show_password'); - // hide/show password and update icon - $(this.dom_).find("input").attr("type", show_password ? "text" : "password"); - $(this.dom_).find("#toggle-show-password").attr("src", show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"]) + + input.attr("type", show_password ? "text" : "password"); + + const icon = show_password + ? window.icons["eye-closed.svg"] + : window.icons["eye-open.svg"]; + $(this.dom_).find("#toggle-show-password img").attr("src", icon); }); } + + updateStrength(value, bar) { + let strength = 0; + if (value.length > 5) strength++; + if (/[A-Z]/.test(value)) strength++; + if (/[0-9]/.test(value)) strength++; + if (/[^A-Za-z0-9]/.test(value)) strength++; + + let width = "0%"; + let cls = ""; + + if (strength === 1) { width = "33%"; cls = "strength-weak"; } + if (strength === 2) { width = "66%"; cls = "strength-medium"; } + if (strength >= 3) { width = "100%"; cls = "strength-strong"; } + + bar.removeClass("strength-weak strength-medium strength-strong") + .addClass(cls) + .css("width", width); + } });