@design.estate/dees-catalog 3.97.1 → 3.98.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist_bundle/bundle.js +4915 -3518
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-button/dees-button/dees-button.d.ts +35 -0
  4. package/dist_ts_web/elements/00group-button/dees-button/dees-button.js +128 -5
  5. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.d.ts +14 -0
  6. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +271 -117
  7. package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.d.ts +21 -0
  8. package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.js +79 -0
  9. package/dist_ts_web/elements/00group-feedback/dees-spinner/dees-spinner.js +12 -3
  10. package/dist_ts_web/elements/00group-form/dees-form-submit/dees-form-submit.d.ts +9 -0
  11. package/dist_ts_web/elements/00group-form/dees-form-submit/dees-form-submit.js +25 -2
  12. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +15 -0
  13. package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +36 -2
  14. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.d.ts +246 -0
  15. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.js +439 -21
  16. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.js +857 -23
  17. package/package.json +3 -3
  18. package/readme.hints.md +35 -0
  19. package/readme.md +80 -11
  20. package/ts_web/00_commitinfo_data.ts +1 -1
  21. package/ts_web/elements/00group-button/dees-button/dees-button.ts +128 -2
  22. package/ts_web/elements/00group-chart/dees-chart-area/component.ts +276 -116
  23. package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +110 -0
  24. package/ts_web/elements/00group-feedback/dees-spinner/dees-spinner.ts +11 -2
  25. package/ts_web/elements/00group-form/dees-form-submit/dees-form-submit.ts +18 -0
  26. package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +29 -0
  27. package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.ts +418 -19
  28. package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.ts +969 -20
@@ -55,6 +55,10 @@ export class DeesSpinner extends DeesElement {
55
55
  display: block;
56
56
  }
57
57
 
58
+ /* --dees-spinner-color lets a spinner on a coloured surface (an accent button face,
59
+ a toast) match that surface's foreground. It is read as an inherited fallback
60
+ rather than declared on :host, because a :host declaration would outrank the
61
+ value an ancestor sets. Unset consumers keep the previous token. */
58
62
  #loading {
59
63
  position: relative;
60
64
  transition: none;
@@ -62,9 +66,14 @@ export class DeesSpinner extends DeesElement {
62
66
  justify-content: center;
63
67
  align-content: center;
64
68
  background: transparent;
65
- border: 3px solid color-mix(in srgb, var(--dees-color-text-primary) 20%, transparent);
69
+ border: 3px solid
70
+ color-mix(
71
+ in srgb,
72
+ var(--dees-spinner-color, var(--dees-color-text-primary)) 20%,
73
+ transparent
74
+ );
66
75
  border-radius: 50%;
67
- border-top-color: var(--dees-color-text-primary);
76
+ border-top-color: var(--dees-spinner-color, var(--dees-color-text-primary));
68
77
  animation: spin 1s ease-in-out infinite;
69
78
  -webkit-animation: spin 1s ease-in-out infinite;
70
79
  }
@@ -47,6 +47,10 @@ export class DeesFormSubmit extends DeesElement {
47
47
  @property({ type: String })
48
48
  accessor iconPosition: 'left' | 'right' = 'left';
49
49
 
50
+ /** stretches the button face to the full host width — see dees-button.fullWidth */
51
+ @property({ type: Boolean, reflect: true, attribute: 'full-width' })
52
+ accessor fullWidth: boolean = false;
53
+
50
54
  constructor() {
51
55
  super();
52
56
  }
@@ -58,6 +62,12 @@ export class DeesFormSubmit extends DeesElement {
58
62
  :host {
59
63
  display: inline-block;
60
64
  }
65
+ /* The host has to stretch too, or forwarding fullWidth only widens the face inside a
66
+ still shrink-wrapped host and the property looks broken. */
67
+ :host([full-width]) {
68
+ display: block;
69
+ width: 100%;
70
+ }
61
71
  dees-button {
62
72
  width: 100%;
63
73
  }
@@ -73,6 +83,7 @@ export class DeesFormSubmit extends DeesElement {
73
83
  .icon=${this.icon}
74
84
  .iconPosition=${this.iconPosition}
75
85
  .text=${this.text}
86
+ .fullWidth=${this.fullWidth}
76
87
  ?disabled=${this.disabled}
77
88
  @clicked=${this.submit}
78
89
  ></dees-button>
@@ -103,6 +114,13 @@ export class DeesFormSubmit extends DeesElement {
103
114
  }
104
115
  }
105
116
 
117
+ /**
118
+ * NOT a focus method despite the name — it submits. `dees-form.addBehaviours()` calls
119
+ * `getSubmitButton()?.focus()` when Enter is pressed in the last field, and that is the
120
+ * catalog's "Enter submits the form" behaviour. Renaming it would break every form, so it
121
+ * stays; keyboard *focus* reaches the submit through the inner dees-button face, which
122
+ * carries its own tabindex.
123
+ */
106
124
  public async focus() {
107
125
  const domtools = await this.domtoolsPromise;
108
126
  if (!this.disabled) {
@@ -67,6 +67,16 @@ export class DeesInputText extends DeesInputBase {
67
67
  @property({ attribute: false })
68
68
  accessor validConfirmed: boolean = false;
69
69
 
70
+ /**
71
+ * autofill hint forwarded to the inner input element.
72
+ * Empty by default, which renders no `autocomplete` attribute at all — byte-identical to
73
+ * this component before the property existed. Set it to a concrete token list to help
74
+ * password managers (`'username'`, `'current-password'`, `'one-time-code'`) or to opt an
75
+ * input into WebAuthn conditional mediation (`'username webauthn'`).
76
+ */
77
+ @property({ type: String })
78
+ accessor autocomplete: string = '';
79
+
70
80
  private validTimeout: ReturnType<typeof setTimeout> | undefined;
71
81
 
72
82
  public static styles = [
@@ -324,6 +334,25 @@ export class DeesInputText extends DeesInputBase {
324
334
  `;
325
335
  }
326
336
 
337
+ /**
338
+ * `autocomplete` is applied imperatively rather than bound in the template so that the
339
+ * default renders no attribute at all. Binding it would emit `autocomplete=""` on every
340
+ * text input in the catalog, and an empty or `on` value is not universally equivalent to
341
+ * an absent one across browser and password-manager heuristics.
342
+ */
343
+ public updated(_changedProperties: Map<string, any>): void {
344
+ super.updated(_changedProperties);
345
+ const input = this.shadowRoot?.querySelector('input');
346
+ if (!input) {
347
+ return;
348
+ }
349
+ if (this.autocomplete) {
350
+ input.setAttribute('autocomplete', this.autocomplete);
351
+ } else {
352
+ input.removeAttribute('autocomplete');
353
+ }
354
+ }
355
+
327
356
  firstUpdated() {
328
357
  if (this.validationFunction && this.value) {
329
358
  const result = this.validationFunction(this.value);
@@ -1,4 +1,420 @@
1
- import { html } from '@design.estate/dees-element';
1
+ import { html, css, cssManager, customElement, state, DeesElement } from '@design.estate/dees-element';
2
+ import { themeDefaultStyles } from '../../00theme.js';
3
+ import type {
4
+ DeesSimpleLogin,
5
+ IDeesLoginPasskeyContext,
6
+ IDeesLoginProvider,
7
+ IDeesLoginProviderContext,
8
+ TDeesLoginMethod,
9
+ } from './dees-simple-login.js';
10
+
11
+ /**
12
+ * The catalog carries no third-party brand assets, so provider icons are always supplied by
13
+ * the consumer. These are neutral Lucide stand-ins for what a real deployment would pass.
14
+ */
15
+ const demoProviders: IDeesLoginProvider[] = [
16
+ { id: 'idp-global', label: 'idp.global', icon: 'lucide:shieldCheck' },
17
+ { id: 'workspace', label: 'Workspace SSO', icon: 'lucide:building2' },
18
+ { id: 'email-link', label: 'Email link', icon: 'lucide:mail' },
19
+ ];
20
+
21
+ const manyProviders: IDeesLoginProvider[] = [
22
+ ...demoProviders,
23
+ { id: 'security-key', label: 'Security key', icon: 'lucide:usb' },
24
+ { id: 'smartcard', label: 'Smartcard', icon: 'lucide:idCard' },
25
+ { id: 'legacy-ldap', label: 'LDAP', icon: 'lucide:network', disabled: true },
26
+ ];
27
+
28
+ const delay = (msArg: number) => new Promise<void>((resolve) => window.setTimeout(resolve, msArg));
29
+
30
+ type TDemoVariant =
31
+ | 'legacy'
32
+ | 'full'
33
+ | 'passkeyOnly'
34
+ | 'enrollment'
35
+ | 'providerOnly'
36
+ | 'manyProviders'
37
+ | 'eventMode';
38
+
39
+ interface IDemoVariantConfig {
40
+ key: TDemoVariant;
41
+ title: string;
42
+ description: string;
43
+ }
44
+
45
+ const variants: IDemoVariantConfig[] = [
46
+ {
47
+ key: 'legacy',
48
+ title: 'Default (password only)',
49
+ description:
50
+ 'No new properties set. This is the pre-existing component: username, password, the ' +
51
+ 'legacy `login` event and nothing else.',
52
+ },
53
+ {
54
+ key: 'full',
55
+ title: 'Passkey + providers + password',
56
+ description:
57
+ 'Handler mode. Every method has its own busy and error state — the passkey handler ' +
58
+ 'below always rejects, and the password form stays untouched by it.',
59
+ },
60
+ {
61
+ key: 'passkeyOnly',
62
+ title: 'Passkey only',
63
+ description: '`.passkey=${true} .password=${false}` — no credential fallback at all.',
64
+ },
65
+ {
66
+ key: 'enrollment',
67
+ title: 'Passkey enrollment + sign in',
68
+ description:
69
+ "`.passkeyIntents=${['authenticate', 'register']}` renders both ceremonies. " +
70
+ 'This is what Onebox hand-rolls today.',
71
+ },
72
+ {
73
+ key: 'providerOnly',
74
+ title: 'Providers only',
75
+ description: 'A pure OIDC surface. The provider handler resolves after a short delay.',
76
+ },
77
+ {
78
+ key: 'manyProviders',
79
+ title: 'Six providers',
80
+ description:
81
+ 'Four or more providers pair up into two columns instead of growing the card. The ' +
82
+ 'last one is `disabled`.',
83
+ },
84
+ {
85
+ key: 'eventMode',
86
+ title: 'Event mode (no handlers)',
87
+ description:
88
+ 'No handler properties. The component dispatches `passkey-login` / `provider-login` ' +
89
+ 'and the demo drives busy and error state through `setBusy()` and `reportError()`.',
90
+ },
91
+ ];
92
+
93
+ @customElement('dees-simple-login-demo')
94
+ class DeesSimpleLoginDemo extends DeesElement {
95
+ @state()
96
+ accessor variant: TDemoVariant = 'full';
97
+
98
+ @state()
99
+ accessor log: string[] = [];
100
+
101
+ public static styles = [
102
+ themeDefaultStyles,
103
+ cssManager.defaultStyles,
104
+ css`
105
+ :host {
106
+ display: block;
107
+ position: absolute;
108
+ inset: 0;
109
+ font-family: var(--dees-font-family);
110
+ color: var(--dees-color-text-primary);
111
+ background: var(--dees-color-bg-canvas);
112
+ }
113
+
114
+ .layout {
115
+ display: grid;
116
+ grid-template-columns: 320px 1fr;
117
+ height: 100%;
118
+ }
119
+
120
+ .sidebar {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: var(--dees-spacing-md);
124
+ padding: var(--dees-spacing-xl);
125
+ overflow-y: auto;
126
+ border-right: 1px solid var(--dees-color-border-default);
127
+ background: var(--dees-color-bg-secondary);
128
+ }
129
+
130
+ .sidebarTitle {
131
+ font-size: var(--dees-font-control-size);
132
+ font-weight: 600;
133
+ color: var(--dees-color-text-muted);
134
+ text-transform: uppercase;
135
+ letter-spacing: 0.06em;
136
+ }
137
+
138
+ .variant {
139
+ padding: var(--dees-spacing-md);
140
+ border: 1px solid var(--dees-color-border-subtle);
141
+ border-radius: var(--dees-radius-lg);
142
+ corner-shape: var(--dees-corner-shape);
143
+ background: var(--dees-color-bg-primary);
144
+ cursor: pointer;
145
+ transition: border-color var(--dees-transition-fast) var(--dees-ease-standard);
146
+ }
147
+
148
+ .variant:hover {
149
+ border-color: var(--dees-color-border-strong);
150
+ }
151
+
152
+ .variant.selected {
153
+ border-color: var(--dees-color-accent-primary);
154
+ box-shadow: 0 0 0 1px var(--dees-color-accent-primary);
155
+ }
156
+
157
+ .variantTitle {
158
+ font-size: var(--dees-font-control-size);
159
+ font-weight: 600;
160
+ }
161
+
162
+ .variantDescription {
163
+ margin-top: var(--dees-spacing-xs);
164
+ font-size: var(--dees-font-control-size-sm);
165
+ line-height: 1.45;
166
+ color: var(--dees-color-text-muted);
167
+ }
168
+
169
+ .logTitle {
170
+ margin-top: var(--dees-spacing-lg);
171
+ }
172
+
173
+ .logEntries {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: var(--dees-spacing-xs);
177
+ font-family: var(--dees-font-family-mono);
178
+ font-size: var(--dees-font-control-size-sm);
179
+ color: var(--dees-color-text-secondary);
180
+ user-select: text;
181
+ }
182
+
183
+ .stage {
184
+ position: relative;
185
+ overflow: hidden;
186
+ }
187
+
188
+ .authenticated {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: var(--dees-spacing-md);
192
+ align-items: center;
193
+ justify-content: center;
194
+ height: 100%;
195
+ text-align: center;
196
+ }
197
+
198
+ .authenticated h1 {
199
+ margin: 0;
200
+ font-size: var(--dees-font-title2-size);
201
+ }
202
+
203
+ .authenticated p {
204
+ margin: 0;
205
+ color: var(--dees-color-text-muted);
206
+ }
207
+ `,
208
+ ];
209
+
210
+ private get loginElement(): DeesSimpleLogin | null {
211
+ return this.shadowRoot?.querySelector('dees-simple-login') || null;
212
+ }
213
+
214
+ private note(messageArg: string): void {
215
+ this.log = [messageArg, ...this.log].slice(0, 12);
216
+ }
217
+
218
+ public render() {
219
+ return html`
220
+ <div class="layout">
221
+ <div class="sidebar">
222
+ <div class="sidebarTitle">Variants</div>
223
+ ${variants.map(
224
+ (variantArg) => html`
225
+ <div
226
+ class="variant ${this.variant === variantArg.key ? 'selected' : ''}"
227
+ @click=${() => {
228
+ this.variant = variantArg.key;
229
+ this.log = [];
230
+ }}
231
+ >
232
+ <div class="variantTitle">${variantArg.title}</div>
233
+ <div class="variantDescription">${variantArg.description}</div>
234
+ </div>
235
+ `,
236
+ )}
237
+ <div class="sidebarTitle logTitle">Event log</div>
238
+ <div class="logEntries">
239
+ ${this.log.length === 0
240
+ ? html`<div>— nothing yet —</div>`
241
+ : this.log.map((entry) => html`<div>${entry}</div>`)}
242
+ </div>
243
+ </div>
244
+ <div class="stage">${this.renderVariant()}</div>
245
+ </div>
246
+ `;
247
+ }
248
+
249
+ private renderVariant() {
250
+ const shared = html`
251
+ <div class="authenticated">
252
+ <h1>Signed in</h1>
253
+ <p>This is the slotted content that appears after login.</p>
254
+ </div>
255
+ `;
256
+
257
+ if (this.variant === 'legacy') {
258
+ return html`
259
+ <dees-simple-login
260
+ name="My Application"
261
+ @login=${(eventArg: CustomEvent) => {
262
+ const data = eventArg.detail?.data;
263
+ this.note(`login: ${data?.username} / ${'*'.repeat(data?.password?.length || 0)}`);
264
+ void this.loginElement?.switchToSlottedContent();
265
+ }}
266
+ >
267
+ ${shared}
268
+ </dees-simple-login>
269
+ `;
270
+ }
271
+
272
+ if (this.variant === 'full') {
273
+ return html`
274
+ <dees-simple-login
275
+ name="idp.global"
276
+ .passkey=${true}
277
+ .passkeyAvailable=${true}
278
+ .providers=${demoProviders}
279
+ .labels=${{ subheading: 'Choose how you want to continue' }}
280
+ .passkeyLoginHandler=${async (contextArg: IDeesLoginPasskeyContext) => {
281
+ this.note(`passkey handler (${contextArg.mediation})`);
282
+ await delay(900);
283
+ throw new Error('No passkey found for this account.');
284
+ }}
285
+ .providerLoginHandler=${async (contextArg: IDeesLoginProviderContext) => {
286
+ this.note(`provider handler: ${contextArg.providerId}`);
287
+ await delay(900);
288
+ void this.loginElement?.switchToSlottedContent();
289
+ }}
290
+ .passwordLoginHandler=${async () => {
291
+ this.note('password handler');
292
+ await delay(900);
293
+ void this.loginElement?.switchToSlottedContent();
294
+ }}
295
+ @login=${(eventArg: CustomEvent) => {
296
+ this.note(`login event: ${eventArg.detail?.data?.username}`);
297
+ }}
298
+ >
299
+ ${shared}
300
+ </dees-simple-login>
301
+ `;
302
+ }
303
+
304
+ if (this.variant === 'passkeyOnly') {
305
+ return html`
306
+ <dees-simple-login
307
+ name="Onebox"
308
+ .passkey=${true}
309
+ .passkeyAvailable=${true}
310
+ .password=${false}
311
+ .labels=${{ subheading: 'Use a passkey registered for your Onebox account' }}
312
+ .passkeyLoginHandler=${async () => {
313
+ this.note('passkey sign in');
314
+ await delay(900);
315
+ void this.loginElement?.switchToSlottedContent();
316
+ }}
317
+ >
318
+ ${shared}
319
+ </dees-simple-login>
320
+ `;
321
+ }
322
+
323
+ if (this.variant === 'enrollment') {
324
+ return html`
325
+ <dees-simple-login
326
+ name="Onebox"
327
+ .passkey=${true}
328
+ .passkeyAvailable=${true}
329
+ .password=${false}
330
+ .passkeyIntents=${['authenticate', 'register']}
331
+ .labels=${{ subheading: 'Sign in, or enroll this device' }}
332
+ .passkeyLoginHandler=${async () => {
333
+ this.note('passkey authenticate');
334
+ await delay(900);
335
+ throw new Error('This device has no passkey for Onebox yet.');
336
+ }}
337
+ .passkeyRegisterHandler=${async () => {
338
+ this.note('passkey register');
339
+ await delay(900);
340
+ void this.loginElement?.switchToSlottedContent();
341
+ }}
342
+ >
343
+ ${shared}
344
+ </dees-simple-login>
345
+ `;
346
+ }
347
+
348
+ if (this.variant === 'providerOnly') {
349
+ return html`
350
+ <dees-simple-login
351
+ name="login.idp.global"
352
+ .password=${false}
353
+ .providers=${demoProviders}
354
+ .labels=${{ subheading: 'Continue with one of your identity providers' }}
355
+ .providerLoginHandler=${async (contextArg: IDeesLoginProviderContext) => {
356
+ this.note(`provider handler: ${contextArg.providerId}`);
357
+ await delay(900);
358
+ void this.loginElement?.switchToSlottedContent();
359
+ }}
360
+ >
361
+ ${shared}
362
+ </dees-simple-login>
363
+ `;
364
+ }
365
+
366
+ if (this.variant === 'manyProviders') {
367
+ return html`
368
+ <dees-simple-login
369
+ name="login.idp.global"
370
+ .providers=${manyProviders}
371
+ .methodOrder=${['provider', 'password'] as TDeesLoginMethod[]}
372
+ .providerLoginHandler=${async (contextArg: IDeesLoginProviderContext) => {
373
+ this.note(`provider handler: ${contextArg.providerId}`);
374
+ await delay(600);
375
+ throw new Error('Discovery endpoint unreachable.');
376
+ }}
377
+ @login=${() => this.note('login event')}
378
+ >
379
+ ${shared}
380
+ </dees-simple-login>
381
+ `;
382
+ }
383
+
384
+ // eventMode
385
+ return html`
386
+ <dees-simple-login
387
+ name="Cloudly"
388
+ .passkey=${true}
389
+ .passkeyAvailable=${true}
390
+ .providers=${demoProviders}
391
+ @passkey-login=${async (eventArg: CustomEvent) => {
392
+ this.note(`passkey-login event (username: ${eventArg.detail.username || '—'})`);
393
+ const element = this.loginElement;
394
+ element?.setBusy({ method: 'passkey' }, true);
395
+ await delay(900);
396
+ element?.setBusy({ method: 'passkey' }, false);
397
+ element?.reportError({ method: 'passkey' }, 'Consumer-reported: ceremony declined.');
398
+ }}
399
+ @provider-login=${async (eventArg: CustomEvent) => {
400
+ this.note(`provider-login event: ${eventArg.detail.providerId}`);
401
+ const element = this.loginElement;
402
+ const target = { method: 'provider' as const, providerId: eventArg.detail.providerId };
403
+ element?.setBusy(target, true);
404
+ await delay(900);
405
+ element?.setBusy(target, false);
406
+ element?.reportError(target, 'Consumer-reported: redirect blocked.');
407
+ }}
408
+ @login=${(eventArg: CustomEvent) => {
409
+ this.note(`login event: ${eventArg.detail?.data?.username}`);
410
+ this.loginElement?.reportError('password', 'Consumer-reported: invalid credentials.');
411
+ }}
412
+ >
413
+ ${shared}
414
+ </dees-simple-login>
415
+ `;
416
+ }
417
+ }
2
418
 
3
419
  export const demoFunc = () => html`
4
420
  <style>
@@ -15,23 +431,6 @@ export const demoFunc = () => html`
15
431
  }
16
432
  </style>
17
433
  <div class="demo-container">
18
- <dees-simple-login
19
- name="My Application"
20
- @login=${(e: CustomEvent) => {
21
- console.log('Login event received:', e.detail);
22
- const loginData = e.detail?.data || e.detail;
23
- if (loginData?.username && loginData?.password) {
24
- alert(`Login attempted with:\nUsername: ${loginData.username}\nPassword: ${loginData.password}`);
25
- // Here you would typically validate credentials and show the slotted content
26
- } else {
27
- console.error('Invalid login data structure:', e.detail);
28
- }
29
- }}
30
- >
31
- <div style="padding: 40px; text-align: center;">
32
- <h1>Welcome!</h1>
33
- <p>This is the slotted content that appears after login.</p>
34
- </div>
35
- </dees-simple-login>
434
+ <dees-simple-login-demo></dees-simple-login-demo>
36
435
  </div>
37
436
  `;