Skip to content
Snippets Groups Projects
Verified Commit 776015c5 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: new login dialog

parent a5cf92a5
No related branches found
No related tags found
No related merge requests found
Showing
with 2066 additions and 25 deletions
export const projectRoot = "/tmp/monster";
export const sourcePath = "/tmp/monster/source";
export const developmentPath = "/tmp/monster/development";
export const pnpxBin = "/nix/store/sxw7i3pyw8v1ycw2sph0zq2byh1prrwm-nodejs-20.18.1/bin/npx";
export const nodeBin = "/nix/store/057dsl3wh2q8syy5sis0x9y5dksl63mv-nodejs-23.8.0/bin/node";
export const projectRoot = "/home/vs/workspaces/oss/monster/monster";
export const sourcePath = "/home/vs/workspaces/oss/monster/monster/source";
export const developmentPath = "/home/vs/workspaces/oss/monster/monster/development";
export const pnpxBin = "/nix/store/2djgcjvx3c183zcdprylsx9p1a6rmwwk-nodejs-20.18.3/bin/npx";
export const nodeBin = "/nix/store/wwdh9slw3km4a1cpwacanciinz742wxa-nodejs-23.9.0/bin/node";
export const license = "/**" + "\n" +
" * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved." + "\n" +
" * Node module: @schukai/monster" + "\n" +
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>new login form #299</title>
<script src="299.mjs" type="module"></script>
</head>
<body>
<h1>new login form #299</h1>
<p>a brand new login form</p>
<ul>
<li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/299">Issue #299</a></li>
<li><a href="/">Back to overview</a></li>
</ul>
<main style="width: 1000px;display: flex; margin-left:400px ; justify-content: center;">
<div style="width: 400px;display: flex;">
<monster-login
data-monster-option-fetch-login-url="/issue-299/login"
data-monster-option-fetch-digits-url="/issue-299/digits"
data-monster-option-fetch-secondfactor-url="/issue-299/secondFactorDigits"
data-monster-option-fetch-forgotpassword-url="/issue-299/forgot-password"
>
<h2 slot="login-header">Log In</h2>
<p slot="login-header">
Use this form to access your account. Enter specific passwords to trigger errors for testing:
<span style="color: red"><b>403</b> for a 403 error, <b>401</b> for a 401 error, <b>500</b> for a 500 error, and <b>2fa</b> for a two-factor authentication prompt.</span>
</p>
<p slot="login-footer">Two-factor authentication (2FA) enhances the security of your account.</p>
<h2 slot="forgot-password-header">Forgot Password</h2>
<p slot="forgot-password-header">
Reset your password using your email. To simulate an error, use <span style="color: red"><b>500@alvine</b></span>.
if you want to simulate 2fa, use <span style="color: red"><b>2fa@alvine</b></span>.
</p>
<h2 slot="second-factor-header">Two-Factor Authentication (2FA)</h2>
<p slot="second-factor-header">
2FA requires you to enter a code sent to your device to log in. Request a code via email or SMS if needed.
</p>
<p slot="second-factor-footer">This extra step ensures that your account remains secure.
Use <span style="color: red"><b>999999</b> to trigger an error or <b>444444</b> for an unauthorized error.</span></p>
<h2 slot="logged-in-header">Logged In</h2>
<p slot="logged-in-content">
You are now logged in to your account.
</p>
<h2 slot="digits-header">Enter the Digits</h2>
<p slot="digits-header">
Enter the verification digits received on your phone or email.
Use <span style="color: red"><b>999999</b> to trigger an error or <b>444444</b> for an unauthorized error.</span>
</p>
</monster-login>
</div>
</main>
</body>
</html>
/**
* @file development/issues/open/299.mjs
* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/299
* @description new login form
* @issue 299
*/
import "../../../source/components/style/property.pcss";
import "../../../source/components/style/link.pcss";
import "../../../source/components/style/color.pcss";
import "../../../source/components/style/theme.pcss";
import "../../../source/components/style/normalize.pcss";
import "../../../source/components/style/typography.pcss";
import "../../../source/components/form/login.mjs";
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>new digit control #300</title>
<script src="./300.mjs" type="module"></script>
</head>
<body>
<h1>new digit control #300</h1>
<p>a new digit control</p>
<ul>
<li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/300">Issue #300</a></li>
<li><a href="/">Back to overview</a></li>
</ul>
<main style="width: 1000px;display: flex; margin-left:400px ; justify-content: center;">
<div style="width: 400px;display: flex;" >
<monster-digits
value="1234"
data-monster-option-digits="9"
data-monster-option-character-set="0123456789"
>
</monster-digits>
</div>
</main>
</body>
</html>
/**
* @file development/issues/open/300.mjs
* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/300
* @description new digit control
* @issue 300
*/
import "../../../source/components/style/property.pcss";
import "../../../source/components/style/link.pcss";
import "../../../source/components/style/color.pcss";
import "../../../source/components/style/theme.pcss";
import "../../../source/components/style/normalize.pcss";
import "../../../source/components/style/typography.pcss";
import "../../../source/components/form/digits.mjs";
\ No newline at end of file
export default [
{
url: '/issue-299/login',
method: 'post',
rawResponse: async (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const {username, password} = JSON.parse(body);
if (password === '2fa') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'MyAppAuth realm="myapp", auth-type="password", 2fa="required"');
res.end(JSON.stringify({error: '2fa'}));
} else if (password === '401') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 401;
res.end(JSON.stringify({error: 'Not logged in'}));
} else if (password === '403') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 403;
res.end(JSON.stringify({error: 'Logged in, but no permissions'}));
} else if (password === '500') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 500
res.end(JSON.stringify({error: 'Internal server error'}));
} else {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 200;
res.end('{}');
}
});
},
},
{
url: '/issue-299/forgot-password',
method: 'post',
rawResponse: async (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const {mail} = JSON.parse(body);
if (mail === '500@alvine') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 500;
res.end(JSON.stringify({error: 'Internal server error'}));
} else if (mail === '2fa@alvine') {
res.setHeader('X-Password-Reset', 'disabled due to 2fa');
res.setHeader('Content-Type', 'application/json');
res.statusCode = 401;
res.end(JSON.stringify({error: 'unauthorized'}));
} else {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 200;
res.end('{}');
}
});
},
},
{
url: '/issue-299/secondFactorDigits',
method: 'post',
rawResponse: async (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const {digits} = JSON.parse(body);
if (digits === '999999') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 500;
res.end(JSON.stringify({error: 'Internal server error'}));
} else if (digits === '444444') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 401;
res.end(JSON.stringify({error: 'unauthorized'}));
} else {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 200;
res.end('{}');
}
});
},
},
{
url: '/issue-299/digits',
method: 'post',
rawResponse: async (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const {digits} = JSON.parse(body);
if (digits === '999999') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 500;
res.end(JSON.stringify({error: 'Internal server error'}));
} else if (digits === '444444') {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 401;
res.end(JSON.stringify({error: 'unauthorized'}));
} else {
res.setHeader('Content-Type', 'application/json');
res.statusCode = 200;
res.end('{}');
}
});
},
},
];
\ No newline at end of file
......@@ -153,6 +153,28 @@ class Digits extends CustomControl {
static getCSSStyleSheet() {
return [DigitsStyleSheet, InvalidStyleSheet];
}
/**
* @return {void}
*/
focus(options) {
const element = this[digitsElementSymbol].querySelector("input");
if (element) {
element.focus(options);
}
}
/**
* @return {void}
*/
blur() {
const elements = this[digitsElementSymbol].querySelectorAll("input");
if (elements) {
for (const element of elements) {
element.blur();
}
}
}
}
/**
......
......@@ -30,6 +30,7 @@ import "../layout/collapse.mjs";
import "./toggle-switch.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { InvalidStyleSheet } from "./stylesheet/invalid.mjs";
export { FieldSet };
......@@ -148,7 +149,7 @@ class FieldSet extends CustomControl {
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [FieldSetStyleSheet];
return [FieldSetStyleSheet, InvalidStyleSheet];
}
/**
......
......@@ -25,6 +25,7 @@ import { datasourceLinkedElementSymbol } from "../datatable/util.mjs";
import { FormStyleSheet } from "./stylesheet/form.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { getDocument } from "../../dom/util.mjs";
import { InvalidStyleSheet } from "./stylesheet/invalid.mjs";
export { Form };
......@@ -88,7 +89,8 @@ class Form extends DataSet {
},
reportValidity: {
selector: "input,select,textarea,monster-select,monster-toggle-switch",
selector:
"input,select,textarea,monster-select,monster-toggle-switch,monster-password",
},
eventProcessing: true,
......@@ -112,7 +114,7 @@ class Form extends DataSet {
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [FormStyleSheet];
return [FormStyleSheet, InvalidStyleSheet];
}
/**
......
This diff is collapsed.
......@@ -20,6 +20,10 @@ import {
import { PasswordStyleSheet } from "./stylesheet/password.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import "./input-group.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { STATE_OFF, STATE_ON } from "./toggle-switch.mjs";
import { Observer } from "../../types/observer.mjs";
import { internalSymbol } from "../../constants.mjs";
export { Password };
......@@ -57,6 +61,8 @@ export const inputElementSymbol = Symbol("inputIconElement");
* @since 3.89.0
* @copyright schukai GmbH
* @summary A beautiful Password field that can make your life easier and also looks good.
* @fires monster-password-hide
* @fires monster-password-show
*/
class Password extends CustomControl {
/**
......@@ -79,20 +85,23 @@ class Password extends CustomControl {
return this;
}
/**
* The current value of the Switch
* @return {string}
*/
get value() {
return this[inputElementSymbol].value;
const obj = this.getInternalUpdateCloneData();
this.value = obj.value;
return obj.value;
}
/**
* Must be overridden by a derived class and set the value of the control.
*
* This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements.
*
* @param {*} value The value to set.
* @throws {Error} the value setter must be overwritten by the derived class
* Set value
* @property {string} value
*/
set value(value) {
this[inputElementSymbol].value = value;
this.setOption("value", value);
this.setFormValue(value);
}
/**
......@@ -139,6 +148,9 @@ class Password extends CustomControl {
placeholder: "",
},
disabled: false,
value: null,
eventProcessing: true,
});
}
......@@ -250,6 +262,7 @@ function getTemplate() {
inputmode path:inputmode,
aria-required path:aria.required,
aria-placeholder path:aria.placeholder"
data-monster-bind="path:value"
>
<svg viewBox="0 0 16 16" data-monster-role="visible-icon" class="hidden">
......
......@@ -17,10 +17,10 @@
}
[data-monster-role="digits"] {
display: grid;
grid-template-columns: 1fr;
grid-gap: 0.3rem;
display: flex;
gap: 0.3rem;
accent-color: var(--monster-color-secondary-2);
flex-wrap: nowrap;
}
[data-monster-role="digits"] > input {
......@@ -32,7 +32,7 @@
}
.invalid {
outline: 1px solid var(--monster-color-error-2) !important;
outline-color: var(--monster-color-error-2) !important;
}
......@@ -25,10 +25,14 @@
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: var(--monster-border-width) var(--monster-border-style) var(--monster-color-primary-1);
border-bottom: var(--monster-border-width) var(--monster-border-style) var(--monster-color-seashell-3);
margin-bottom: 1rem;
padding-bottom: 0.2rem;
@media (prefers-color-scheme: dark) {
border-color: var(--monster-color-secondary-1);
}
& label {
padding-right: 0.3rem;
@mixin text;
......@@ -134,6 +138,10 @@ slot {
box-sizing: border-box;
}
::slotted(monster-password) {
min-height: 1.8rem;
}
::slotted(input[type="color"]) {
padding: 0 0.2rem;
min-height: calc(1.8rem + 0.4rem);
......@@ -160,6 +168,7 @@ slot {
::slotted(input),
::slotted(monster-toggle-switch),
::slotted(monster-password),
::slotted(select) {
align-self: end;
}
......@@ -210,8 +219,10 @@ slot {
grid-column: 1;
}
::slotted(div.wrapper),
::slotted(input),
::slotted(monster-toggle-switch),
::slotted(monster-password),
::slotted(textarea),
::slotted(select) {
grid-column: 1;
......
:host(:invalid) {
position: relative;
}
:host(:invalid)::after {
content: "";
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 12px;
background: var(--monster-color-error-2);
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 11"><defs><linearGradient id="opacityGradient" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="150" y2="0"><stop offset="0%" stop-opacity="1"/><stop offset="50%" stop-opacity="0.5"/><stop offset="100%" stop-opacity="1"/></linearGradient><linearGradient id="widthGradient" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="150" y2="0"><stop offset="0%" stop-color="#000" stop-opacity="1" style="stop-color: black; stop-opacity: 1"/><stop offset="50%" stop-color="#000" stop-opacity="0.3" style="stop-color: black; stop-opacity: 0.3"/><stop offset="100%" stop-color="#000" stop-opacity="1" style="stop-color: black; stop-opacity: 1"/></linearGradient></defs><path d="M0,6 C25,2 50,10 75,6 100,2 125,10 150,6" stroke="url(#opacityGradient)" stroke-width="2" fill="none" stroke-linecap="round" stroke-dasharray="url(#widthGradient)"/></svg>');
mask-repeat: repeat-x;
background-size: 150px auto;
}
input:invalid,
textarea:invalid,
select:invalid {
outline-color: var(--monster-color-error-2);
}
\ No newline at end of file
@import "../../style/color.pcss";
@import "../../style/theme.pcss";
@import "../../style/border.pcss";
@import "../../style/form.pcss";
@import "../../style/control.pcss";
@import "../../style/badge.pcss";
@import '../../style/mixin/typography.pcss';
@import '../../style/typography.pcss';
@import '../../style/mixin/hover.pcss';
@import "../../style/control.pcss";
@import "../../style/floating-ui.pcss";
:host {
display: block;
width: 100%;
padding: 0;
margin: 0;
box-sizing: border-box;
max-width: 500px;
}
[data-monster-role="control"] {
width: 100%;
box-sizing: border-box;
}
monster-message-state-button {
margin-top: 2.9rem;
}
.hidden {
display: none !important;
}
[data-monster-role="forgot-password-link"],
[data-monster-role="reset-login-process-link"],
[data-monster-role="login-link"] {
display: block;
margin-top: 1.5rem;
text-align: center;
cursor: pointer;
color: var(--monster-color-secondary-1);
text-decoration: none;
outline: none;
transition: color 300ms ease-in-out, text-decoration-color 300ms ease-in-out;
}
a, a:link, a:visited, a:hover, a:active, a:focus {
color: var(--monster-color-secondary-1);
text-decoration: none;
outline: none;
transition: color 300ms ease-in-out, text-decoration-color 300ms ease-in-out;
}
a:hover, a:active, a:focus {
color: var(--monster-color-primary-2);
text-decoration: underline;
text-decoration-color: var(--monster-color-secondary-1 );
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a:focus {
outline: 1px dashed var(--monster-color-selection-1);
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
a, a:link, a:visited, a:hover, a:active, a:focus {
color: var(--monster-color-amber-2);
}
a:focus {
outline: 1px dashed var(--monster-color-selection-4);
}
}
.invalid {
position: relative;
}
.invalid::after {
content: "";
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 12px;
background: var(--monster-color-error-2);
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 11"><defs><linearGradient id="opacityGradient" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="150" y2="0"><stop offset="0%" stop-opacity="1"/><stop offset="50%" stop-opacity="0.5"/><stop offset="100%" stop-opacity="1"/></linearGradient><linearGradient id="widthGradient" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="150" y2="0"><stop offset="0%" stop-color="#000" stop-opacity="1" style="stop-color: black; stop-opacity: 1"/><stop offset="50%" stop-color="#000" stop-opacity="0.3" style="stop-color: black; stop-opacity: 0.3"/><stop offset="100%" stop-color="#000" stop-opacity="1" style="stop-color: black; stop-opacity: 1"/></linearGradient></defs><path d="M0,6 C25,2 50,10 75,6 100,2 125,10 150,6" stroke="url(#opacityGradient)" stroke-width="2" fill="none" stroke-linecap="round" stroke-dasharray="url(#widthGradient)"/></svg>');
mask-repeat: repeat-x;
background-size: 150px auto;
}
::slotted(h1),
::slotted(h2),
::slotted(h3),
::slotted(h4),
::slotted(h5),
::slotted(h6) {
margin-top: 0 !important;
}
\ No newline at end of file
......@@ -7,6 +7,8 @@
input:focus-visible,
input {
outline: none;
color: var(--monster-color-primary-1);
background-color: var(--monster-bg-color-primary-1);
}
svg {
......
This diff is collapsed.
This diff is collapsed.
/**
* Copyright © schukai GmbH and all contributing authors, 2025. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*/
import { addAttributeToken } from "../../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
export { InvalidStyleSheet };
/**
* @private
* @type {CSSStyleSheet}
*/
const InvalidStyleSheet = new CSSStyleSheet();
try {
InvalidStyleSheet.insertRule(
`
@layer invalid {
:host(:invalid){position:relative}:host(:invalid):after{background:var(--monster-color-error-2);background-size:150px auto;bottom:-12px;content:\"\";height:12px;left:0;-webkit-mask-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 150 11\"><defs><linearGradient id=\"a\" x1=\"0\" x2=\"150\" y1=\"0\" y2=\"0\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0%\"/><stop offset=\"50%\" stop-opacity=\".5\"/><stop offset=\"100%\"/></linearGradient></defs><path fill=\"none\" stroke=\"url(%23a)\" stroke-dasharray=\"url(%23widthGradient)\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"M0 6c25-4 50 4 75 0s50 4 75 0\"/></svg>');mask-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 150 11\"><defs><linearGradient id=\"a\" x1=\"0\" x2=\"150\" y1=\"0\" y2=\"0\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0%\"/><stop offset=\"50%\" stop-opacity=\".5\"/><stop offset=\"100%\"/></linearGradient></defs><path fill=\"none\" stroke=\"url(%23a)\" stroke-dasharray=\"url(%23widthGradient)\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"M0 6c25-4 50 4 75 0s50 4 75 0\"/></svg>');-webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;position:absolute;width:100%}input:invalid,select:invalid,textarea:invalid{outline-color:var(--monster-color-error-2)}
}`,
0,
);
} catch (e) {
addAttributeToken(
document.getRootNode().querySelector("html"),
ATTRIBUTE_ERRORMESSAGE,
e + "",
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment