@pure-ds/core 0.7.53 → 0.7.55
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/dist/types/pds.d.ts +9 -6
- package/dist/types/public/assets/pds/components/pds-fab.d.ts +45 -6
- package/dist/types/public/assets/pds/components/pds-fab.d.ts.map +1 -1
- package/dist/types/public/assets/pds/components/pds-toaster.d.ts +12 -2
- package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -1
- package/dist/types/src/js/common/common.d.ts.map +1 -1
- package/dist/types/src/js/common/toast.d.ts +20 -10
- package/dist/types/src/js/common/toast.d.ts.map +1 -1
- package/package.json +1 -1
- package/public/assets/js/app.js +5 -5
- package/public/assets/js/pds-ask.js +9 -9
- package/public/assets/js/pds-manager.js +78 -78
- package/public/assets/js/pds.js +2 -2
- package/public/assets/pds/components/pds-fab.js +427 -142
- package/public/assets/pds/components/pds-toaster.js +22 -1
- package/public/assets/pds/core/pds-ask.js +9 -9
- package/public/assets/pds/core/pds-manager.js +78 -78
- package/public/assets/pds/core.js +2 -2
- package/src/js/common/common.js +20 -4
- package/src/js/common/toast.js +14 -5
- package/src/js/pds-core/pds-enhancers-meta.js +135 -135
- package/src/js/pds.d.ts +9 -6
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PDS } from "#pds";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Floating Action Button (FAB) with expandable satellite actions
|
|
3
5
|
*
|
|
@@ -15,6 +17,10 @@
|
|
|
15
17
|
* @cssprop --sat-fg - Foreground color of satellites (default: var(--color-text-primary))
|
|
16
18
|
* @cssprop --radius - Distance of satellites from main FAB (default: 100px)
|
|
17
19
|
* @cssprop --transition-duration - Animation duration (default: 420ms)
|
|
20
|
+
* @cssprop --z-fab-in-drawer - FAB z-index when used inside pds-drawer (default: var(--z-popover))
|
|
21
|
+
*
|
|
22
|
+
* @attr {"click"|"hover"} behavior - Interaction mode for opening satellites (default: click)
|
|
23
|
+
* @attr {"fixed"|"inline"} mode - Layout mode for FAB positioning (default: fixed)
|
|
18
24
|
*
|
|
19
25
|
* @csspart fab - The main FAB button
|
|
20
26
|
* @csspart satellite - Individual satellite buttons
|
|
@@ -31,6 +37,11 @@ export class PdsFab extends HTMLElement {
|
|
|
31
37
|
#hasCustomSpread = false;
|
|
32
38
|
#hasCustomStartAngle = false;
|
|
33
39
|
#iconRetryPending = false;
|
|
40
|
+
#behavior = 'click';
|
|
41
|
+
#mode = 'fixed';
|
|
42
|
+
#fabClass = '';
|
|
43
|
+
#hoverMoveTracker = null;
|
|
44
|
+
#componentStyles = null;
|
|
34
45
|
|
|
35
46
|
constructor() {
|
|
36
47
|
super();
|
|
@@ -39,7 +50,7 @@ export class PdsFab extends HTMLElement {
|
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
static get observedAttributes() {
|
|
42
|
-
return ['open', 'radius', 'spread', 'start-angle'];
|
|
53
|
+
return ['open', 'radius', 'spread', 'start-angle', 'behavior', 'mode', 'fab-class'];
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
@@ -109,10 +120,10 @@ export class PdsFab extends HTMLElement {
|
|
|
109
120
|
/**
|
|
110
121
|
* Starting angle in degrees (0=right, 90=down, 180=left, 270=up)
|
|
111
122
|
* If not specified, the angle is auto-detected based on the FAB's corner position:
|
|
112
|
-
* - Bottom-right: 180
|
|
113
|
-
* - Bottom-left: 315
|
|
114
|
-
* - Top-right: 225
|
|
115
|
-
* - Top-left: 45
|
|
123
|
+
* - Bottom-right: 180-� (fly left/up)
|
|
124
|
+
* - Bottom-left: 315-� (fly right/up)
|
|
125
|
+
* - Top-right: 225-� (fly left/down)
|
|
126
|
+
* - Top-left: 45-� (fly right/down)
|
|
116
127
|
* @type {number}
|
|
117
128
|
* @attr start-angle
|
|
118
129
|
* @default 180 (or auto-detected)
|
|
@@ -130,9 +141,78 @@ export class PdsFab extends HTMLElement {
|
|
|
130
141
|
this.#render();
|
|
131
142
|
}
|
|
132
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Interaction mode for satellite menu.
|
|
146
|
+
* - click: toggles satellites when main FAB is clicked
|
|
147
|
+
* - hover: opens on hover/focus and closes when pointer leaves safe area
|
|
148
|
+
* @type {'click'|'hover'}
|
|
149
|
+
* @attr behavior
|
|
150
|
+
* @default click
|
|
151
|
+
*/
|
|
152
|
+
get behavior() {
|
|
153
|
+
return this.#behavior;
|
|
154
|
+
}
|
|
155
|
+
set behavior(val) {
|
|
156
|
+
const next = val === 'hover' ? 'hover' : 'click';
|
|
157
|
+
if (this.#behavior === next) return;
|
|
158
|
+
this.#behavior = next;
|
|
159
|
+
if (this.getAttribute('behavior') !== next) {
|
|
160
|
+
this.setAttribute('behavior', next);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.#render();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Layout mode for host positioning.
|
|
168
|
+
* - fixed: FAB is anchored to viewport bottom-right
|
|
169
|
+
* - inline: FAB participates in normal document flow
|
|
170
|
+
* @type {'fixed'|'inline'}
|
|
171
|
+
* @attr mode
|
|
172
|
+
* @default fixed
|
|
173
|
+
*/
|
|
174
|
+
get mode() {
|
|
175
|
+
return this.#mode;
|
|
176
|
+
}
|
|
177
|
+
set mode(val) {
|
|
178
|
+
const next = val === 'inline' ? 'inline' : 'fixed';
|
|
179
|
+
if (this.#mode === next) return;
|
|
180
|
+
this.#mode = next;
|
|
181
|
+
if (this.getAttribute('mode') !== next) {
|
|
182
|
+
this.setAttribute('mode', next);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.#render();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Space-separated class list forwarded to the internal button.
|
|
190
|
+
* Use this instead of host classes for btn-* and icon-only.
|
|
191
|
+
* @type {string}
|
|
192
|
+
* @attr fab-class
|
|
193
|
+
* @example "btn-primary icon-only"
|
|
194
|
+
*/
|
|
195
|
+
get fabClass() {
|
|
196
|
+
return this.#fabClass;
|
|
197
|
+
}
|
|
198
|
+
set fabClass(val) {
|
|
199
|
+
const next = String(val || '').trim();
|
|
200
|
+
if (this.#fabClass === next) return;
|
|
201
|
+
this.#fabClass = next;
|
|
202
|
+
if (this.getAttribute('fab-class') !== next) {
|
|
203
|
+
if (next) {
|
|
204
|
+
this.setAttribute('fab-class', next);
|
|
205
|
+
} else {
|
|
206
|
+
this.removeAttribute('fab-class');
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.#render();
|
|
211
|
+
}
|
|
212
|
+
|
|
133
213
|
/**
|
|
134
214
|
* Array of satellite button configurations
|
|
135
|
-
|
|
215
|
+
* @type {Array<{key: string, icon?: string, iconColor?: string, bgColor?: string, iconSize?: string, label?: string, action?: string}>}
|
|
136
216
|
*/
|
|
137
217
|
get satellites() {
|
|
138
218
|
return this.#satellites;
|
|
@@ -184,18 +264,257 @@ export class PdsFab extends HTMLElement {
|
|
|
184
264
|
this.#hasCustomStartAngle = true;
|
|
185
265
|
this.startAngle = Number(newVal) || 180;
|
|
186
266
|
break;
|
|
267
|
+
case 'behavior':
|
|
268
|
+
this.behavior = newVal === 'hover' ? 'hover' : 'click';
|
|
269
|
+
break;
|
|
270
|
+
case 'mode':
|
|
271
|
+
this.mode = newVal === 'inline' ? 'inline' : 'fixed';
|
|
272
|
+
break;
|
|
273
|
+
case 'fab-class':
|
|
274
|
+
this.fabClass = newVal || '';
|
|
275
|
+
break;
|
|
187
276
|
}
|
|
188
277
|
}
|
|
189
278
|
|
|
190
|
-
connectedCallback() {
|
|
279
|
+
async connectedCallback() {
|
|
191
280
|
this.#hasCustomRadius = this.hasAttribute('radius');
|
|
192
281
|
this.#hasCustomSpread = this.hasAttribute('spread');
|
|
193
282
|
this.#hasCustomStartAngle = this.hasAttribute('start-angle');
|
|
283
|
+
this.#behavior = this.getAttribute('behavior') === 'hover' ? 'hover' : 'click';
|
|
284
|
+
this.#mode = this.getAttribute('mode') === 'inline' ? 'inline' : 'fixed';
|
|
285
|
+
this.#fabClass = (this.getAttribute('fab-class') || '').trim();
|
|
286
|
+
if (!this.#componentStyles) {
|
|
287
|
+
this.#componentStyles = PDS.createStylesheet(/*css*/`
|
|
288
|
+
@layer pds-fab {
|
|
289
|
+
:host {
|
|
290
|
+
--fab-hit-area: var(--control-min-height, 2.5rem);
|
|
291
|
+
--sat-size: 48px;
|
|
292
|
+
--fab-size: 64px;
|
|
293
|
+
--fab-bg: var(--color-primary-600, #0078d4);
|
|
294
|
+
--fab-fg: white;
|
|
295
|
+
--sat-bg: var(--color-surface-elevated, #2a2a2a);
|
|
296
|
+
--sat-fg: var(--color-text-primary, #fff);
|
|
297
|
+
--radius: 100px;
|
|
298
|
+
--transition-duration: 420ms;
|
|
299
|
+
|
|
300
|
+
position: fixed;
|
|
301
|
+
inset: auto 20px 20px auto;
|
|
302
|
+
z-index: var(--fab-host-z-index, var(--z-notification, 1000));
|
|
303
|
+
display: inline-block;
|
|
304
|
+
vertical-align: middle;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
:host([mode="inline"]) {
|
|
308
|
+
position: relative;
|
|
309
|
+
inset: auto;
|
|
310
|
+
--fab-size: auto;
|
|
311
|
+
--sat-size: 40px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.wrap {
|
|
315
|
+
position: relative;
|
|
316
|
+
width: var(--fab-size);
|
|
317
|
+
height: var(--fab-size);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
:host([mode="inline"]) .wrap {
|
|
321
|
+
width: auto;
|
|
322
|
+
height: auto;
|
|
323
|
+
display: inline-flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
justify-content: center;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.fab {
|
|
329
|
+
position: relative;
|
|
330
|
+
z-index: 2;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.fab ::slotted(pds-icon) {
|
|
334
|
+
display: inline-block;
|
|
335
|
+
transition: transform 0.2s ease;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* When FAB is icon-only, the slot must fill and center for proper button geometry */
|
|
339
|
+
.fab.icon-only ::slotted(pds-icon) {
|
|
340
|
+
display: grid;
|
|
341
|
+
place-items: center;
|
|
342
|
+
inline-size: 100%;
|
|
343
|
+
block-size: 100%;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
:host([mode="inline"]) .fab.icon-only {
|
|
347
|
+
/* Match compact icon-only geometry used by normal PDS buttons */
|
|
348
|
+
inline-size: max(36px, calc(var(--font-size-base) + (max(calc(var(--spacing-1) * 1), var(--spacing-2)) * 2) + (var(--border-width-medium) * 2)));
|
|
349
|
+
block-size: max(36px, calc(var(--font-size-base) + (max(calc(var(--spacing-1) * 1), var(--spacing-2)) * 2) + (var(--border-width-medium) * 2)));
|
|
350
|
+
min-inline-size: 0;
|
|
351
|
+
min-block-size: 0;
|
|
352
|
+
padding: 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
:host([open]) .fab.has-satellites.has-plus ::slotted(pds-icon) {
|
|
356
|
+
transform: rotate(45deg);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.sat-text {
|
|
360
|
+
font-size: 1.25rem;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
user-select: none;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Host may carry forwarded button classes, but must never render as a button */
|
|
367
|
+
:host([class*="btn-"]),
|
|
368
|
+
:host(.icon-only) {
|
|
369
|
+
background: transparent;
|
|
370
|
+
border: 0;
|
|
371
|
+
border-radius: 0;
|
|
372
|
+
box-shadow: none;
|
|
373
|
+
color: inherit;
|
|
374
|
+
font: inherit;
|
|
375
|
+
inline-size: auto;
|
|
376
|
+
block-size: auto;
|
|
377
|
+
min-inline-size: 0;
|
|
378
|
+
min-block-size: 0;
|
|
379
|
+
padding: 0;
|
|
380
|
+
text-decoration: none;
|
|
381
|
+
transform: none;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* Unlayered to prevent generic button primitives from overriding satellites */
|
|
385
|
+
.sat {
|
|
386
|
+
all: unset;
|
|
387
|
+
position: absolute;
|
|
388
|
+
top: calc((var(--fab-size) - var(--sat-size)) / 2);
|
|
389
|
+
left: calc((var(--fab-size) - var(--sat-size)) / 2);
|
|
390
|
+
inline-size: var(--sat-size);
|
|
391
|
+
block-size: var(--sat-size);
|
|
392
|
+
border-radius: 50%;
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
background: var(--sat-bg);
|
|
395
|
+
color: var(--sat-fg);
|
|
396
|
+
box-shadow:
|
|
397
|
+
0 2px 6px rgba(0, 0, 0, 0.15),
|
|
398
|
+
0 6px 18px rgba(0, 0, 0, 0.2);
|
|
399
|
+
display: grid;
|
|
400
|
+
place-items: center;
|
|
401
|
+
cursor: pointer;
|
|
402
|
+
transform: translate(0, 0) scale(0.2);
|
|
403
|
+
opacity: 0;
|
|
404
|
+
pointer-events: none;
|
|
405
|
+
transition:
|
|
406
|
+
transform 420ms cubic-bezier(0.2, 0.8, 0.2, 1.4),
|
|
407
|
+
opacity 420ms ease;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
:host([mode="inline"]) .sat {
|
|
411
|
+
inline-size: 40px;
|
|
412
|
+
block-size: 40px;
|
|
413
|
+
border-radius: 50%;
|
|
414
|
+
--sat-icon-offset-x: 0px;
|
|
415
|
+
--sat-icon-offset-y: 3px;
|
|
416
|
+
background: var(--sat-bg-color, var(--sat-bg, var(--color-primary-600, #0078d4)));
|
|
417
|
+
color: var(--sat-icon-color, #fff);
|
|
418
|
+
box-shadow:
|
|
419
|
+
0 2px 6px rgba(0, 0, 0, 0.18),
|
|
420
|
+
0 8px 16px rgba(0, 0, 0, 0.16);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
:host([mode="inline"]) .sat pds-icon {
|
|
424
|
+
color: var(--sat-icon-color, #fff);
|
|
425
|
+
filter: none;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
:host([open]) .sat {
|
|
429
|
+
transform: translate(var(--tx, 0), var(--ty, 0)) scale(1);
|
|
430
|
+
opacity: 1;
|
|
431
|
+
pointer-events: auto;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.sat:hover,
|
|
435
|
+
.sat:focus-visible {
|
|
436
|
+
transform: translate(var(--tx, 0), var(--ty, 0)) scale(1.1);
|
|
437
|
+
outline: 2px solid var(--color-accent-400, #0078d4);
|
|
438
|
+
outline-offset: 2px;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.sat pds-icon,
|
|
442
|
+
.sat ::slotted(pds-icon) {
|
|
443
|
+
display: block;
|
|
444
|
+
margin: 0;
|
|
445
|
+
transform: translate(var(--sat-icon-offset-x, 0px), var(--sat-icon-offset-y, 0px));
|
|
446
|
+
pointer-events: none;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.sat > slot {
|
|
450
|
+
position: absolute;
|
|
451
|
+
inset: 0;
|
|
452
|
+
display: grid;
|
|
453
|
+
place-items: center;
|
|
454
|
+
inline-size: 100%;
|
|
455
|
+
block-size: 100%;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
:host([mode="inline"]) .sat[data-key="like"] {
|
|
459
|
+
background: var(--sat-bg-color, var(--color-primary-600, #0078d4));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
:host([mode="inline"]) .sat[data-key="love"] {
|
|
463
|
+
background: var(--sat-bg-color, var(--color-danger-600, #b42318));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
:host([mode="inline"]) .sat[data-key="interesting"] {
|
|
467
|
+
background: var(--sat-bg-color, var(--color-warning-600, #b54708));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
:host(:not([mode="inline"])) .fab {
|
|
471
|
+
appearance: none;
|
|
472
|
+
border: none;
|
|
473
|
+
border-radius: 50%;
|
|
474
|
+
inline-size: var(--fab-size);
|
|
475
|
+
block-size: var(--fab-size);
|
|
476
|
+
background: var(--fab-bg);
|
|
477
|
+
color: var(--fab-fg);
|
|
478
|
+
box-shadow:
|
|
479
|
+
0 2px 8px rgba(0, 0, 0, 0.15),
|
|
480
|
+
0 8px 24px rgba(0, 0, 0, 0.25);
|
|
481
|
+
display: grid;
|
|
482
|
+
place-items: center;
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
position: relative;
|
|
485
|
+
z-index: 2;
|
|
486
|
+
transition: transform 0.2s ease, box-shadow 0.3s ease;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
:host(:not([mode="inline"])) .fab:hover {
|
|
490
|
+
transform: scale(1.05);
|
|
491
|
+
box-shadow:
|
|
492
|
+
0 4px 12px rgba(0, 0, 0, 0.2),
|
|
493
|
+
0 12px 32px rgba(0, 0, 0, 0.3);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
:host(:not([mode="inline"])) .fab:active {
|
|
497
|
+
transform: scale(0.98);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
:host(:not([mode="inline"])[open]) .fab {
|
|
501
|
+
box-shadow:
|
|
502
|
+
0 0 0 2px var(--color-accent-400, #0078d4),
|
|
503
|
+
0 4px 16px rgba(0, 120, 212, 0.4);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
await PDS.adoptLayers(this.shadowRoot, ['primitives', 'components'], [this.#componentStyles]);
|
|
194
509
|
this.#render();
|
|
195
510
|
}
|
|
196
511
|
|
|
197
512
|
disconnectedCallback() {
|
|
198
513
|
document.removeEventListener('click', this.#outsideClickBound, true);
|
|
514
|
+
if (this.#hoverMoveTracker) {
|
|
515
|
+
document.removeEventListener('pointermove', this.#hoverMoveTracker);
|
|
516
|
+
this.#hoverMoveTracker = null;
|
|
517
|
+
}
|
|
199
518
|
}
|
|
200
519
|
|
|
201
520
|
/**
|
|
@@ -253,24 +572,44 @@ export class PdsFab extends HTMLElement {
|
|
|
253
572
|
const isBottom = rect.bottom > viewportHeight / 2;
|
|
254
573
|
|
|
255
574
|
// Calculate optimal angle based on corner:
|
|
256
|
-
// - Bottom-right (default): 180
|
|
257
|
-
// - Bottom-left: 315
|
|
258
|
-
// - Top-right: 225
|
|
259
|
-
// - Top-left: 45
|
|
575
|
+
// - Bottom-right (default): 180-� (fly left/up)
|
|
576
|
+
// - Bottom-left: 315-� or -45-� (fly right/up)
|
|
577
|
+
// - Top-right: 225-� (fly left/down)
|
|
578
|
+
// - Top-left: 45-� (fly right/down)
|
|
260
579
|
|
|
261
580
|
if (isBottom && isRight) {
|
|
262
|
-
return 180; // Bottom-right
|
|
581
|
+
return 180; // Bottom-right ��� fly left
|
|
263
582
|
} else if (isBottom && !isRight) {
|
|
264
|
-
return 315; // Bottom-left
|
|
583
|
+
return 315; // Bottom-left ��� fly up-right
|
|
265
584
|
} else if (!isBottom && isRight) {
|
|
266
|
-
return 225; // Top-right
|
|
585
|
+
return 225; // Top-right ��� fly left-down
|
|
267
586
|
} else {
|
|
268
|
-
return 45; // Top-left
|
|
587
|
+
return 45; // Top-left ��� fly right-down
|
|
269
588
|
}
|
|
270
589
|
}
|
|
271
590
|
|
|
272
591
|
#render() {
|
|
592
|
+
if (this.#hoverMoveTracker) {
|
|
593
|
+
document.removeEventListener('pointermove', this.#hoverMoveTracker);
|
|
594
|
+
this.#hoverMoveTracker = null;
|
|
595
|
+
}
|
|
596
|
+
|
|
273
597
|
const count = this.satellites.length;
|
|
598
|
+
const isInDrawer = Boolean(this.closest('pds-drawer'));
|
|
599
|
+
const isInlineMode = this.#mode === 'inline';
|
|
600
|
+
const isIconOnlyFab = this.#fabClass.split(/\s+/).includes('icon-only');
|
|
601
|
+
const hostPosition = isInlineMode ? 'relative' : 'fixed';
|
|
602
|
+
const hostInset = isInlineMode ? 'auto' : 'auto 20px 20px auto';
|
|
603
|
+
const hostZIndex = isInDrawer
|
|
604
|
+
? 'var(--z-fab-in-drawer, var(--z-popover, 1060))'
|
|
605
|
+
: 'var(--z-notification, 1000)';
|
|
606
|
+
|
|
607
|
+
this.style.setProperty('--fab-host-z-index', hostZIndex);
|
|
608
|
+
|
|
609
|
+
const forwardedClasses = this.#fabClass
|
|
610
|
+
.split(/\s+/)
|
|
611
|
+
.filter(Boolean)
|
|
612
|
+
.join(' ');
|
|
274
613
|
|
|
275
614
|
// Auto-adjust spread and radius if not explicitly set by user
|
|
276
615
|
const hasCustomRadius = this.#hasCustomRadius;
|
|
@@ -291,6 +630,11 @@ export class PdsFab extends HTMLElement {
|
|
|
291
630
|
if (!hasCustomSpread) activeSpread = optimal.spread;
|
|
292
631
|
}
|
|
293
632
|
|
|
633
|
+
// Keep inline icon-only FAB satellites tighter to the trigger button.
|
|
634
|
+
if (isInlineMode && isIconOnlyFab && !hasCustomRadius) {
|
|
635
|
+
activeRadius = Math.min(activeRadius, 56);
|
|
636
|
+
}
|
|
637
|
+
|
|
294
638
|
// Calculate positions along an arc
|
|
295
639
|
const step = count > 1 ? activeSpread / (count - 1) : 0;
|
|
296
640
|
const baseAngle = hasCustomStartAngle || typeof activeStartAngle === 'number'
|
|
@@ -300,131 +644,9 @@ export class PdsFab extends HTMLElement {
|
|
|
300
644
|
? baseAngle
|
|
301
645
|
: baseAngle - (activeSpread / 2);
|
|
302
646
|
|
|
303
|
-
this.
|
|
304
|
-
<style>
|
|
305
|
-
:host {
|
|
306
|
-
--sat-size: 48px;
|
|
307
|
-
--fab-size: 64px;
|
|
308
|
-
--fab-bg: var(--color-primary-600, #0078d4);
|
|
309
|
-
--fab-fg: white;
|
|
310
|
-
--sat-bg: var(--color-surface-elevated, #2a2a2a);
|
|
311
|
-
--sat-fg: var(--color-text-primary, #fff);
|
|
312
|
-
--radius: ${activeRadius}px;
|
|
313
|
-
--transition-duration: 420ms;
|
|
314
|
-
|
|
315
|
-
position: fixed;
|
|
316
|
-
inset: auto 20px 20px auto;
|
|
317
|
-
z-index: var(--z-notification, 1000);
|
|
318
|
-
display: inline-block;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
.wrap {
|
|
322
|
-
position: relative;
|
|
323
|
-
width: var(--fab-size);
|
|
324
|
-
height: var(--fab-size);
|
|
325
|
-
}
|
|
647
|
+
this.style.setProperty('--radius', `${activeRadius}px`);
|
|
326
648
|
|
|
327
|
-
|
|
328
|
-
.fab {
|
|
329
|
-
appearance: none;
|
|
330
|
-
border: none;
|
|
331
|
-
border-radius: 50%;
|
|
332
|
-
inline-size: var(--fab-size);
|
|
333
|
-
block-size: var(--fab-size);
|
|
334
|
-
background: var(--fab-bg);
|
|
335
|
-
color: var(--fab-fg);
|
|
336
|
-
box-shadow:
|
|
337
|
-
0 2px 8px rgba(0, 0, 0, 0.15),
|
|
338
|
-
0 8px 24px rgba(0, 0, 0, 0.25);
|
|
339
|
-
display: grid;
|
|
340
|
-
place-items: center;
|
|
341
|
-
cursor: pointer;
|
|
342
|
-
transition: transform 0.2s ease, box-shadow 0.3s ease;
|
|
343
|
-
position: relative;
|
|
344
|
-
z-index: 2;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.fab:hover {
|
|
348
|
-
transform: scale(1.05);
|
|
349
|
-
box-shadow:
|
|
350
|
-
0 4px 12px rgba(0, 0, 0, 0.2),
|
|
351
|
-
0 12px 32px rgba(0, 0, 0, 0.3);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
.fab:active {
|
|
355
|
-
transform: scale(0.98);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
:host([open]) .fab {
|
|
359
|
-
box-shadow:
|
|
360
|
-
0 0 0 2px var(--color-accent-400, #0078d4),
|
|
361
|
-
0 4px 16px rgba(0, 120, 212, 0.4);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/* Rotate plus icon 45deg when open to look like X */
|
|
365
|
-
.fab ::slotted(pds-icon) {
|
|
366
|
-
display: inline-block;
|
|
367
|
-
transition: transform 0.2s ease;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
:host([open]) .fab.has-satellites.has-plus ::slotted(pds-icon) {
|
|
371
|
-
transform: rotate(45deg);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/* SATELLITES */
|
|
375
|
-
.sat {
|
|
376
|
-
position: absolute;
|
|
377
|
-
top: calc((var(--fab-size) - var(--sat-size)) / 2);
|
|
378
|
-
left: calc((var(--fab-size) - var(--sat-size)) / 2);
|
|
379
|
-
inline-size: var(--sat-size);
|
|
380
|
-
block-size: var(--sat-size);
|
|
381
|
-
border-radius: 50%;
|
|
382
|
-
background: var(--sat-bg);
|
|
383
|
-
color: var(--sat-fg);
|
|
384
|
-
box-shadow:
|
|
385
|
-
0 2px 6px rgba(0, 0, 0, 0.15),
|
|
386
|
-
0 6px 18px rgba(0, 0, 0, 0.2);
|
|
387
|
-
display: grid;
|
|
388
|
-
place-items: center;
|
|
389
|
-
cursor: pointer;
|
|
390
|
-
border: none;
|
|
391
|
-
appearance: none;
|
|
392
|
-
|
|
393
|
-
transform: translate(0, 0) scale(0.2);
|
|
394
|
-
opacity: 0;
|
|
395
|
-
pointer-events: none;
|
|
396
|
-
|
|
397
|
-
transition:
|
|
398
|
-
transform 420ms cubic-bezier(0.2, 0.8, 0.2, 1.4),
|
|
399
|
-
opacity 420ms ease;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
:host([open]) .sat {
|
|
403
|
-
transform: translate(var(--tx, 0), var(--ty, 0)) scale(1);
|
|
404
|
-
opacity: 1;
|
|
405
|
-
pointer-events: auto;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.sat:hover,
|
|
410
|
-
.sat:focus-visible {
|
|
411
|
-
transform: translate(var(--tx, 0), var(--ty, 0)) scale(1.1);
|
|
412
|
-
outline: 2px solid var(--color-accent-400, #0078d4);
|
|
413
|
-
outline-offset: 2px;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.sat pds-icon,
|
|
417
|
-
.sat ::slotted(pds-icon) {
|
|
418
|
-
pointer-events: none;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/* Fallback text for satellites without icons */
|
|
422
|
-
.sat-text {
|
|
423
|
-
font-size: 1.25rem;
|
|
424
|
-
font-weight: 600;
|
|
425
|
-
user-select: none;
|
|
426
|
-
}
|
|
427
|
-
</style>
|
|
649
|
+
this.shadowRoot.innerHTML = `
|
|
428
650
|
|
|
429
651
|
<div class="wrap" role="group" aria-label="Floating actions">
|
|
430
652
|
${this.satellites.map((sat, i) => {
|
|
@@ -438,14 +660,14 @@ export class PdsFab extends HTMLElement {
|
|
|
438
660
|
<button
|
|
439
661
|
class="sat"
|
|
440
662
|
part="satellite"
|
|
441
|
-
style="--tx: ${tx}px; --ty: ${ty}px; transition-delay: ${delay}ms
|
|
663
|
+
style="--tx: ${tx}px; --ty: ${ty}px; transition-delay: ${delay}ms;${sat.bgColor ? ` --sat-bg-color: ${sat.bgColor};` : ''}${sat.iconColor ? ` --sat-icon-color: ${sat.iconColor};` : ''}"
|
|
442
664
|
data-key="${sat.key}"
|
|
443
665
|
aria-label="${sat.label || sat.key}"
|
|
444
666
|
title="${sat.label || sat.key}"
|
|
445
667
|
>
|
|
446
668
|
<slot name="satellite-${sat.key}">
|
|
447
669
|
${sat.icon
|
|
448
|
-
? `<pds-icon icon="${sat.icon}"></pds-icon>`
|
|
670
|
+
? `<pds-icon icon="${sat.icon}" size="${sat.iconSize || (isInlineMode ? 'md' : 'md')}"${sat.iconColor && !isInlineMode ? ` color="${sat.iconColor}"` : ''}></pds-icon>`
|
|
449
671
|
: `<span class="sat-text">${(sat.label || sat.key).charAt(0).toUpperCase()}</span>`
|
|
450
672
|
}
|
|
451
673
|
</slot>
|
|
@@ -454,7 +676,7 @@ export class PdsFab extends HTMLElement {
|
|
|
454
676
|
}).join('')}
|
|
455
677
|
|
|
456
678
|
<button
|
|
457
|
-
class="fab${count > 0 ? ' has-satellites' : ''}"
|
|
679
|
+
class="fab${count > 0 ? ' has-satellites' : ''}${forwardedClasses ? ' ' + forwardedClasses : ''}"
|
|
458
680
|
part="fab"
|
|
459
681
|
aria-expanded="${this.#open}"
|
|
460
682
|
aria-haspopup="menu"
|
|
@@ -476,6 +698,61 @@ export class PdsFab extends HTMLElement {
|
|
|
476
698
|
|
|
477
699
|
const wrap = this.shadowRoot.querySelector('.wrap');
|
|
478
700
|
wrap?.addEventListener('keydown', (e) => this.#onKey(e));
|
|
701
|
+
|
|
702
|
+
this.#syncFabClasses();
|
|
703
|
+
|
|
704
|
+
if (this.#behavior === 'hover') {
|
|
705
|
+
const stopTracking = () => {
|
|
706
|
+
if (this.#hoverMoveTracker) {
|
|
707
|
+
document.removeEventListener('pointermove', this.#hoverMoveTracker);
|
|
708
|
+
this.#hoverMoveTracker = null;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const isPointerSafe = (x, y) => {
|
|
713
|
+
const fabEl = this.shadowRoot?.querySelector('.fab');
|
|
714
|
+
const satEls = Array.from(this.shadowRoot?.querySelectorAll('.sat') ?? []);
|
|
715
|
+
const padding = 40;
|
|
716
|
+
for (const el of [fabEl, ...satEls].filter(Boolean)) {
|
|
717
|
+
const r = el.getBoundingClientRect();
|
|
718
|
+
if (x >= r.left - padding && x <= r.right + padding &&
|
|
719
|
+
y >= r.top - padding && y <= r.bottom + padding) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return false;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const startTracking = () => {
|
|
727
|
+
stopTracking();
|
|
728
|
+
this.#hoverMoveTracker = (e) => {
|
|
729
|
+
if (!isPointerSafe(e.clientX, e.clientY)) {
|
|
730
|
+
stopTracking();
|
|
731
|
+
this.open = false;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
document.addEventListener('pointermove', this.#hoverMoveTracker, { passive: true });
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
wrap?.addEventListener('mouseenter', () => {
|
|
738
|
+
stopTracking();
|
|
739
|
+
if (count > 0) this.open = true;
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
wrap?.addEventListener('mouseleave', startTracking);
|
|
743
|
+
|
|
744
|
+
wrap?.addEventListener('focusin', () => {
|
|
745
|
+
stopTracking();
|
|
746
|
+
if (count > 0) this.open = true;
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
wrap?.addEventListener('focusout', (e) => {
|
|
750
|
+
if (!wrap.contains(e.relatedTarget)) {
|
|
751
|
+
stopTracking();
|
|
752
|
+
this.open = false;
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
479
756
|
|
|
480
757
|
// Handle icon swapping when slot content changes (main FAB slot only)
|
|
481
758
|
const fabSlot = this.shadowRoot.querySelector('.fab slot');
|
|
@@ -484,6 +761,14 @@ export class PdsFab extends HTMLElement {
|
|
|
484
761
|
requestAnimationFrame(() => this.#updateIconState());
|
|
485
762
|
}
|
|
486
763
|
|
|
764
|
+
#syncFabClasses() {
|
|
765
|
+
const fab = this.shadowRoot?.querySelector('.fab');
|
|
766
|
+
if (!fab) return;
|
|
767
|
+
const baseClasses = Array.from(fab.classList).filter((c) => c === 'fab' || c === 'has-satellites' || c === 'has-plus');
|
|
768
|
+
const forwarded = this.#fabClass.split(/\s+/).filter(Boolean);
|
|
769
|
+
fab.className = [...baseClasses, ...forwarded].join(' ');
|
|
770
|
+
}
|
|
771
|
+
|
|
487
772
|
#updateSatelliteTransitionDelays(isOpen) {
|
|
488
773
|
const sats = this.shadowRoot?.querySelectorAll('.sat');
|
|
489
774
|
if (!sats?.length) return;
|