@pure-ds/storybook 0.1.0

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 (129) hide show
  1. package/.storybook/addons/description/preview.js +15 -0
  2. package/.storybook/addons/description/register.js +60 -0
  3. package/.storybook/addons/html-preview/Panel.jsx +327 -0
  4. package/.storybook/addons/html-preview/constants.js +6 -0
  5. package/.storybook/addons/html-preview/preview.js +178 -0
  6. package/.storybook/addons/html-preview/register.js +16 -0
  7. package/.storybook/addons/pds-configurator/SearchTool.js +44 -0
  8. package/.storybook/addons/pds-configurator/Tool.js +30 -0
  9. package/.storybook/addons/pds-configurator/constants.js +9 -0
  10. package/.storybook/addons/pds-configurator/preview.js +159 -0
  11. package/.storybook/addons/pds-configurator/register.js +24 -0
  12. package/.storybook/docs.css +35 -0
  13. package/.storybook/htmlPreview.css +103 -0
  14. package/.storybook/htmlPreview.js +271 -0
  15. package/.storybook/main.js +160 -0
  16. package/.storybook/preview-body.html +48 -0
  17. package/.storybook/preview-head.html +11 -0
  18. package/.storybook/preview.js +1563 -0
  19. package/README.md +266 -0
  20. package/bin/index.js +40 -0
  21. package/dist/pds-reference.json +2101 -0
  22. package/package.json +45 -0
  23. package/pds.config.js +6 -0
  24. package/public/assets/css/app.css +1216 -0
  25. package/public/assets/data/auto-design-advanced.json +704 -0
  26. package/public/assets/data/auto-design-simple.json +123 -0
  27. package/public/assets/img/icon-512x512.png +0 -0
  28. package/public/assets/img/logo-trans.png +0 -0
  29. package/public/assets/img/logo.png +0 -0
  30. package/public/assets/js/app.js +15088 -0
  31. package/public/assets/js/app.js.map +7 -0
  32. package/public/assets/js/lit.js +1176 -0
  33. package/public/assets/js/lit.js.map +7 -0
  34. package/public/assets/js/pds.js +9801 -0
  35. package/public/assets/js/pds.js.map +7 -0
  36. package/public/assets/pds/components/pds-calendar.js +837 -0
  37. package/public/assets/pds/components/pds-drawer.js +857 -0
  38. package/public/assets/pds/components/pds-icon.js +338 -0
  39. package/public/assets/pds/components/pds-jsonform.js +1775 -0
  40. package/public/assets/pds/components/pds-richtext.js +1035 -0
  41. package/public/assets/pds/components/pds-scrollrow.js +331 -0
  42. package/public/assets/pds/components/pds-splitpanel.js +401 -0
  43. package/public/assets/pds/components/pds-tabstrip.js +251 -0
  44. package/public/assets/pds/components/pds-toaster.js +446 -0
  45. package/public/assets/pds/components/pds-upload.js +657 -0
  46. package/public/assets/pds/custom-elements.json +2003 -0
  47. package/public/assets/pds/icons/pds-icons.svg +498 -0
  48. package/public/assets/pds/pds-css-complete.json +1861 -0
  49. package/public/assets/pds/pds-runtime-config.json +11 -0
  50. package/public/assets/pds/pds.css-data.json +2152 -0
  51. package/public/assets/pds/styles/pds-components.css +1944 -0
  52. package/public/assets/pds/styles/pds-components.css.js +3895 -0
  53. package/public/assets/pds/styles/pds-primitives.css +352 -0
  54. package/public/assets/pds/styles/pds-primitives.css.js +711 -0
  55. package/public/assets/pds/styles/pds-styles.css +3761 -0
  56. package/public/assets/pds/styles/pds-styles.css.js +7529 -0
  57. package/public/assets/pds/styles/pds-tokens.css +699 -0
  58. package/public/assets/pds/styles/pds-tokens.css.js +1405 -0
  59. package/public/assets/pds/styles/pds-utilities.css +763 -0
  60. package/public/assets/pds/styles/pds-utilities.css.js +1533 -0
  61. package/public/assets/pds/vscode-custom-data.json +824 -0
  62. package/scripts/build-pds-reference.mjs +807 -0
  63. package/scripts/generate-stories.js +542 -0
  64. package/scripts/package-build.js +86 -0
  65. package/src/js/app.js +17 -0
  66. package/src/js/common/ask.js +208 -0
  67. package/src/js/common/common.js +20 -0
  68. package/src/js/common/font-loader.js +200 -0
  69. package/src/js/common/msg.js +90 -0
  70. package/src/js/lit.js +40 -0
  71. package/src/js/pds-core/pds-config.js +1162 -0
  72. package/src/js/pds-core/pds-enhancer-metadata.js +75 -0
  73. package/src/js/pds-core/pds-enhancers.js +357 -0
  74. package/src/js/pds-core/pds-enums.js +86 -0
  75. package/src/js/pds-core/pds-generator.js +5317 -0
  76. package/src/js/pds-core/pds-ontology.js +256 -0
  77. package/src/js/pds-core/pds-paths.js +109 -0
  78. package/src/js/pds-core/pds-query.js +571 -0
  79. package/src/js/pds-core/pds-registry.js +129 -0
  80. package/src/js/pds-core/pds.d.ts +129 -0
  81. package/src/js/pds.d.ts +408 -0
  82. package/src/js/pds.js +1579 -0
  83. package/src/pds-core/pds-api.js +105 -0
  84. package/stories/GettingStarted.md +96 -0
  85. package/stories/GettingStarted.stories.js +144 -0
  86. package/stories/WhatIsPDS.md +194 -0
  87. package/stories/WhatIsPDS.stories.js +144 -0
  88. package/stories/components/PdsCalendar.stories.js +263 -0
  89. package/stories/components/PdsDrawer.stories.js +623 -0
  90. package/stories/components/PdsIcon.stories.js +78 -0
  91. package/stories/components/PdsJsonform.stories.js +1444 -0
  92. package/stories/components/PdsRichtext.stories.js +367 -0
  93. package/stories/components/PdsScrollrow.stories.js +140 -0
  94. package/stories/components/PdsSplitpanel.stories.js +502 -0
  95. package/stories/components/PdsTabstrip.stories.js +442 -0
  96. package/stories/components/PdsToaster.stories.js +186 -0
  97. package/stories/components/PdsUpload.stories.js +66 -0
  98. package/stories/enhancements/Dropdowns.stories.js +185 -0
  99. package/stories/enhancements/InteractiveStates.stories.js +625 -0
  100. package/stories/enhancements/MeshGradients.stories.js +320 -0
  101. package/stories/enhancements/OpenGroups.stories.js +227 -0
  102. package/stories/enhancements/RangeSliders.stories.js +232 -0
  103. package/stories/enhancements/RequiredFields.stories.js +189 -0
  104. package/stories/enhancements/Toggles.stories.js +167 -0
  105. package/stories/foundations/Colors.stories.js +283 -0
  106. package/stories/foundations/Icons.stories.js +305 -0
  107. package/stories/foundations/SmartSurfaces.stories.js +367 -0
  108. package/stories/foundations/Spacing.stories.js +175 -0
  109. package/stories/foundations/Typography.stories.js +960 -0
  110. package/stories/foundations/ZIndex.stories.js +325 -0
  111. package/stories/patterns/BorderEffects.stories.js +72 -0
  112. package/stories/patterns/Layout.stories.js +99 -0
  113. package/stories/patterns/Utilities.stories.js +107 -0
  114. package/stories/primitives/Accordion.stories.js +359 -0
  115. package/stories/primitives/Alerts.stories.js +64 -0
  116. package/stories/primitives/Badges.stories.js +183 -0
  117. package/stories/primitives/Buttons.stories.js +229 -0
  118. package/stories/primitives/Cards.stories.js +353 -0
  119. package/stories/primitives/FormGroups.stories.js +569 -0
  120. package/stories/primitives/Forms.stories.js +131 -0
  121. package/stories/primitives/Media.stories.js +203 -0
  122. package/stories/primitives/Tables.stories.js +232 -0
  123. package/stories/reference/ReferenceCatalog.stories.js +28 -0
  124. package/stories/reference/reference-catalog.js +413 -0
  125. package/stories/reference/reference-docs.js +302 -0
  126. package/stories/reference/reference-helpers.js +310 -0
  127. package/stories/utilities/GridSystem.stories.js +208 -0
  128. package/stories/utils/PdsAsk.stories.js +420 -0
  129. package/stories/utils/toast-utils.js +148 -0
@@ -0,0 +1,857 @@
1
+ const PDS = window.PDS;
2
+
3
+ /**
4
+ * @element pds-drawer
5
+ * @fires toggle - Fired when the drawer opens or closes
6
+ *
7
+ * @slot drawer-header - Header content for the drawer
8
+ * @slot drawer-content - Main content of the drawer
9
+ *
10
+ * @cssprop --drawer-duration - Animation duration (default: var(--transition-normal))
11
+ * @cssprop --drawer-easing - Animation easing function (default: var(--easing-emphasized))
12
+ * @cssprop --drawer-max-height - Maximum height when position is top/bottom (default: 70vh)
13
+ * @cssprop --drawer-min-height - Minimum height when position is top/bottom (default: auto)
14
+ *
15
+ * @csspart backdrop - The semi-transparent backdrop overlay
16
+ * @csspart panel - The drawer panel container
17
+ * @csspart header - The drawer header section
18
+ * @csspart close-button - The close button
19
+ * @csspart grab-handle - The drag handle indicator
20
+ * @csspart content - The drawer content section
21
+ */
22
+ customElements.define("pds-drawer", class extends HTMLElement {
23
+ #isDragging = false;
24
+ #startX = 0;
25
+ #startY = 0;
26
+ #lastX = 0;
27
+ #lastY = 0;
28
+ #lastTS = 0;
29
+ #velocity = 0; // px/ms along active axis
30
+ #startFraction = 0;
31
+ #aside = null;
32
+ #drawerHeight = 0;
33
+ #drawerWidth = 0;
34
+ #raf = 0;
35
+ #currentFraction = 0; // 0=open, 1=closed
36
+ #resizeObs = null;
37
+ #openAnimationController = null;
38
+ constructor() {
39
+ super();
40
+ this.attachShadow({ mode: "open" });
41
+ // default state
42
+ this._open = false;
43
+ this._position = "bottom"; // bottom | top | left | right
44
+ this._drag = "header"; // header | none
45
+ this._maxHeight = "";
46
+ this._minHeight = "";
47
+ this._showClose = false;
48
+ }
49
+ static get observedAttributes() {
50
+ return [
51
+ "open",
52
+ "position",
53
+ "drag",
54
+ "max-height",
55
+ "min-height",
56
+ "show-close",
57
+ ];
58
+ }
59
+
60
+ // Attribute/property reflection
61
+
62
+ /**
63
+ * Controls whether the drawer is open or closed
64
+ * @type {boolean}
65
+ * @attr open
66
+ */
67
+ get open() {
68
+ return this._open;
69
+ }
70
+ set open(val) {
71
+ const bool = Boolean(val);
72
+ if (this._open === bool) return;
73
+ this._open = bool;
74
+ this.toggleAttribute("open", this._open);
75
+ if (this._open) {
76
+ queueMicrotask(() => this.#aside?.focus());
77
+ document.body.classList.add("drawer-open");
78
+ }
79
+ else {
80
+ document.body.classList.remove("drawer-open");
81
+ }
82
+ this.dispatchEvent(new Event("toggle"));
83
+ this.#syncAria();
84
+ }
85
+
86
+ /**
87
+ * Position of the drawer relative to the viewport
88
+ * @type {"bottom" | "top" | "left" | "right"}
89
+ * @attr position
90
+ * @default "bottom"
91
+ */
92
+ get position() {
93
+ return this._position;
94
+ }
95
+ set position(val) {
96
+ const v = String(val || "bottom");
97
+ if (this._position === v) return;
98
+ this._position = v;
99
+ this.setAttribute("position", v);
100
+ this.#applyFraction(this.#currentFraction, false);
101
+ this.#renderCloseButtonVisibility();
102
+ }
103
+
104
+ /**
105
+ * Controls drag interaction behavior
106
+ * @type {"header" | "none"}
107
+ * @attr drag
108
+ * @default "header"
109
+ */
110
+ get drag() {
111
+ return this._drag;
112
+ }
113
+ set drag(val) {
114
+ const v = String(val || "header");
115
+ if (this._drag === v) return;
116
+ this._drag = v;
117
+ this.setAttribute("drag", v);
118
+ }
119
+
120
+ /**
121
+ * Maximum height for top/bottom positioned drawers (CSS value)
122
+ * @type {string}
123
+ * @attr max-height
124
+ * @default "70vh"
125
+ */
126
+ get maxHeight() {
127
+ return this._maxHeight;
128
+ }
129
+ set maxHeight(val) {
130
+ this._maxHeight = val || "";
131
+ if (this.#aside) {
132
+ this.#aside.style.setProperty(
133
+ "--drawer-max-height",
134
+ this._maxHeight || "70vh"
135
+ );
136
+ this.#recalc();
137
+ }
138
+ if (this._maxHeight) this.setAttribute("max-height", this._maxHeight);
139
+ else this.removeAttribute("max-height");
140
+ }
141
+
142
+ /**
143
+ * Minimum height for top/bottom positioned drawers (CSS value)
144
+ * @type {string}
145
+ * @attr min-height
146
+ * @default "auto"
147
+ */
148
+ get minHeight() {
149
+ return this._minHeight;
150
+ }
151
+ set minHeight(val) {
152
+ this._minHeight = val || "";
153
+ if (this.#aside) {
154
+ this.#aside.style.setProperty(
155
+ "--drawer-min-height",
156
+ this._minHeight || "auto"
157
+ );
158
+ this.#recalc();
159
+ }
160
+ if (this._minHeight) this.setAttribute("min-height", this._minHeight);
161
+ else this.removeAttribute("min-height");
162
+ }
163
+
164
+ /**
165
+ * Whether to show the close button in the header
166
+ * @type {boolean}
167
+ * @attr show-close
168
+ * @default false
169
+ */
170
+ get showClose() {
171
+ return this._showClose;
172
+ }
173
+ set showClose(val) {
174
+ const bool = Boolean(val);
175
+ this._showClose = bool;
176
+ this.toggleAttribute("show-close", this._showClose);
177
+ this.#renderCloseButtonVisibility();
178
+ }
179
+
180
+ attributeChangedCallback(name, _old, value) {
181
+ switch (name) {
182
+ case "open":
183
+ this._open = this.hasAttribute("open");
184
+ if (this._open) {
185
+ this.#queueOpenAnimation();
186
+ } else {
187
+ this.#cancelPendingOpenAnimation();
188
+ this.#animateTo(1);
189
+ }
190
+ this.#syncAria();
191
+ break;
192
+ case "position":
193
+ this._position = value || "bottom";
194
+ this.#applyFraction(this.#currentFraction, false);
195
+ this.#renderCloseButtonVisibility();
196
+ break;
197
+ case "drag":
198
+ this._drag = value || "header";
199
+ break;
200
+ case "max-height":
201
+ this._maxHeight = value || "";
202
+ if (this.#aside)
203
+ this.#aside.style.setProperty(
204
+ "--drawer-max-height",
205
+ this._maxHeight || "70vh"
206
+ );
207
+ break;
208
+ case "min-height":
209
+ this._minHeight = value || "";
210
+ if (this.#aside)
211
+ this.#aside.style.setProperty(
212
+ "--drawer-min-height",
213
+ this._minHeight || "auto"
214
+ );
215
+ break;
216
+ case "show-close":
217
+ this._showClose = this.hasAttribute("show-close");
218
+ this.#renderCloseButtonVisibility();
219
+ break;
220
+ }
221
+ }
222
+
223
+ async connectedCallback() {
224
+ if (!this.shadowRoot) this.attachShadow({ mode: "open" });
225
+
226
+ // Set default position attribute if not explicitly set
227
+ if (!this.hasAttribute('position')) {
228
+ this.setAttribute('position', 'bottom');
229
+ }
230
+
231
+ // Compose shadow DOM
232
+ this.shadowRoot.innerHTML = /*html*/`
233
+ <div class="backdrop" part="backdrop"></div>
234
+ <div class="layer" id="layer" aria-hidden="true">
235
+ <aside part="panel" tabindex="-1">
236
+ <header part="header">
237
+ <button class="close-btn" part="close-button" aria-label="Close drawer" hidden>
238
+ <pds-icon icon="x" size="sm"></pds-icon>
239
+ </button>
240
+ <slot name="drawer-header"></slot>
241
+ <div class="grab-handle" part="grab-handle" aria-hidden="true"></div>
242
+ </header>
243
+ <div part="content">
244
+ <slot name="drawer-content"></slot>
245
+ </div>
246
+ </aside>
247
+ </div>
248
+ `;
249
+
250
+ // Adopt PDS layers + component stylesheet
251
+ const componentStyles = PDS.createStylesheet(/*css*/ `
252
+ @layer pds-drawer {
253
+ :host { position: fixed; inset: 0; display: contents; contain: layout style size; }
254
+
255
+ /* Timing tokens */
256
+ :host { --_dur: var(--drawer-duration, var(--transition-normal)); }
257
+ :host { --_easing: var(--drawer-easing, var(--easing-emphasized, cubic-bezier(0.25,1,0.5,1))); }
258
+
259
+ ::slotted(*) {
260
+ padding: var(--spacing-4);
261
+ background-color: var(--color-surface-overlay);
262
+ }
263
+
264
+ /* Backdrop */
265
+ .backdrop {
266
+ position: fixed; inset: 0;
267
+ background: var(--backdrop-bg, var(--color-scrim, color-mix(in oklab, CanvasText 20%, Canvas 80%)));
268
+ backdrop-filter: var(--backdrop-filter, none);
269
+ opacity: 0; pointer-events: none; visibility: hidden;
270
+ transition: opacity var(--_dur) var(--_easing), visibility 0s var(--_dur);
271
+ z-index: var(--z-modal);
272
+ }
273
+ :host([open]) .backdrop { opacity: var(--backdrop-opacity); pointer-events: auto; visibility: visible; transition-delay: 0s; }
274
+
275
+ /* Layer container */
276
+ .layer {
277
+ position: fixed; left: 0; right: 0; width: 100%; max-width: 100%;
278
+ contain: layout paint style; will-change: transform;
279
+ z-index: var(--z-drawer);
280
+ display: flex; align-items: flex-end;
281
+ pointer-events: none; visibility: hidden;
282
+ transition: visibility 0s var(--_dur);
283
+ }
284
+ :host([open]) .layer { pointer-events: auto; visibility: visible; transition-delay: 0s; }
285
+ :host([position="bottom"]) .layer { bottom: 0; height: auto; }
286
+ :host([position="top"]) .layer { top: 0; height: auto; align-items: flex-start; }
287
+
288
+ /* Left/Right layout */
289
+ :host([position="left"]) .layer, :host([position="right"]) .layer {
290
+ top: 0; bottom: 0; translate: none;
291
+ width: var(--drawer-width, min(90vw, 28rem));
292
+ max-width: var(--drawer-width, min(90vw, 28rem));
293
+ }
294
+ :host([position="left"]) .layer { left: 0; right: auto; }
295
+ :host([position="right"]) .layer { right: 0; left: auto; }
296
+
297
+ /* Panel */
298
+ aside {
299
+ display: flex; flex-direction: column;
300
+ background: var(--drawer-bg, var(--color-surface-overlay, Canvas));
301
+ box-shadow: var(--drawer-shadow, var(--shadow-xl));
302
+ max-height: var(--drawer-max-height, 70vh);
303
+ min-height: var(--drawer-min-height, auto);
304
+ width: 100%; max-width: 100%;
305
+ margin: 0;
306
+ border-radius: var(--drawer-radius, var(--radius-lg));
307
+ overflow: clip; contain: layout paint style; will-change: transform;
308
+ touch-action: none;
309
+ outline: none;
310
+ }
311
+ :host([position="bottom"]) aside {
312
+ border-bottom-left-radius: 0; border-bottom-right-radius: 0;
313
+ }
314
+ :host([position="top"]) aside {
315
+ flex-direction: column-reverse;
316
+ border-top-left-radius: 0; border-top-right-radius: 0;
317
+ }
318
+ :host([position="left"]) aside {
319
+ border-top-left-radius: 0; border-bottom-left-radius: 0;
320
+ max-height: 100vh; height: 100%; width: 100%;
321
+ }
322
+ :host([position="right"]) aside {
323
+ border-top-right-radius: 0; border-bottom-right-radius: 0;
324
+ max-height: 100vh; height: 100%; width: 100%;
325
+ }
326
+
327
+ header {
328
+ position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
329
+ min-block-size: var(--drawer-header-min-hit, var(--control-min-height, var(--spacing-10)));
330
+ }
331
+ .grab-handle {
332
+ order: -1; /* Put grab handle first (top) by default */
333
+ inline-size: var(--drawer-handle-width, var(--size-9, var(--spacing-9)));
334
+ block-size: var(--drawer-handle-height, var(--size-1, var(--spacing-1)));
335
+ border-radius: var(--drawer-handle-radius, var(--radius-full));
336
+ background: var(--drawer-handle-bg, var(--color-border));
337
+ opacity: 0.9; pointer-events: none; user-select: none;
338
+ }
339
+ :host([position="left"]) .grab-handle, :host([position="right"]) .grab-handle { display:none; }
340
+ :host([position="top"]) .grab-handle { order: 1; } /* Put grab handle last (bottom visually) for top position */
341
+
342
+ .close-btn {
343
+ position: absolute; right: var(--spacing-2); top: 50%; transform: translateY(-50%);
344
+ display: inline-flex; align-items: center; justify-content: center;
345
+ width: var(--size-8, var(--spacing-8)); height: var(--size-8, var(--spacing-8));
346
+ border-radius: var(--radius-sm);
347
+ border: none; background: transparent; color: inherit; cursor: pointer;
348
+ }
349
+ .close-btn:hover { opacity: 0.85; }
350
+ .close-btn:focus { outline: var(--focus-outline, none); }
351
+ ::slotted([slot="drawer-header"]) { inline-size: 100%; display: block; min-block-size: var(--drawer-header-min-hit, var(--control-min-height, var(--spacing-10))); }
352
+
353
+ [part="content"] { flex: 1; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; contain: layout paint style; }
354
+
355
+ main { overflow: auto; -webkit-overflow-scrolling: touch; contain: layout paint style; transition: height var(--_dur) var(--_easing); }
356
+
357
+ @media (min-width: 800px) {
358
+ aside { width: 100%; max-width: 800px; margin-inline: auto; border-radius: var(--drawer-radius, var(--radius-lg)); overflow: hidden; }
359
+ }
360
+ }
361
+ `);
362
+
363
+ await PDS.adoptLayers(this.shadowRoot, ["primitives", "components"], [componentStyles]);
364
+
365
+ // References
366
+ this.#aside = this.shadowRoot.querySelector("aside");
367
+ this.#applyFraction(this.open ? 0 : 1, false);
368
+ this.#syncAria();
369
+ this.#renderCloseButtonVisibility();
370
+
371
+ // Wire events
372
+ const backdrop = this.shadowRoot.querySelector('.backdrop');
373
+ backdrop?.addEventListener('click', this.#onBackdropClick);
374
+
375
+ const aside = this.#aside;
376
+ if (aside) aside.addEventListener('pointerdown', (e) => {
377
+ if (this._drag === 'none') return;
378
+ // Only allow drag from header when configured
379
+ if (this._drag === 'header') {
380
+ const header = this.shadowRoot.querySelector('header');
381
+ const path = e.composedPath();
382
+ if (!path.includes(header)) return;
383
+ }
384
+ this.#onPointerDown(e);
385
+ });
386
+
387
+ // Global listeners
388
+ window.addEventListener("pointermove", this.#onPointerMove, { passive: false });
389
+ window.addEventListener("pointerup", this.#onPointerUp, { passive: true });
390
+ window.addEventListener("keydown", this.#onKeyDown);
391
+
392
+ // Resize observers
393
+ this.#resizeObs = new ResizeObserver(this.#recalc);
394
+ this.#resizeObs.observe(this.#aside);
395
+ window.addEventListener("resize", this.#recalc, { passive: true });
396
+ if (window.visualViewport) window.visualViewport.addEventListener("resize", this.#recalc, { passive: true });
397
+
398
+ this.#recalc();
399
+ }
400
+
401
+ disconnectedCallback() {
402
+ // Clean up global listeners
403
+ window.removeEventListener("pointermove", this.#onPointerMove);
404
+ window.removeEventListener("pointerup", this.#onPointerUp);
405
+ window.removeEventListener("keydown", this.#onKeyDown);
406
+ if (window.visualViewport)
407
+ window.visualViewport.removeEventListener("resize", this.#recalc);
408
+ window.removeEventListener("resize", this.#recalc);
409
+ this.#resizeObs?.disconnect();
410
+ cancelAnimationFrame(this.#raf);
411
+ this.#cancelPendingOpenAnimation();
412
+ }
413
+
414
+ // Public API
415
+
416
+ /**
417
+ * Opens the drawer
418
+ * @method openDrawer
419
+ * @public
420
+ */
421
+ openDrawer() {
422
+ this.open = true;
423
+ }
424
+
425
+ /**
426
+ * Closes the drawer
427
+ * @method closeDrawer
428
+ * @public
429
+ */
430
+ closeDrawer() {
431
+ this.open = false;
432
+ }
433
+
434
+ /**
435
+ * Toggles the drawer open/closed state
436
+ * @method toggleDrawer
437
+ * @public
438
+ */
439
+ toggleDrawer() {
440
+ this.open = !this.open;
441
+ }
442
+
443
+ /**
444
+ * Configure and open the drawer in one call
445
+ * @method show
446
+ * @public
447
+ * @param {any|HTMLElement|string} htmlContent - The main content to display
448
+ * @param {Object} [options] - Configuration options
449
+ * @param {any|HTMLElement|string} [options.header] - Header content
450
+ * @param {"bottom"|"top"|"left"|"right"} [options.position] - Drawer position
451
+ * @param {string} [options.maxHeight] - Maximum height (CSS value)
452
+ * @param {string} [options.minHeight] - Minimum height (CSS value)
453
+ * @param {boolean} [options.showClose] - Show close button
454
+ * @param {boolean} [options.waitForMedia=true] - Wait for images/videos to load
455
+ * @param {number} [options.mediaTimeout=500] - Media load timeout in ms
456
+ * @returns {Promise<this>} Resolves to the drawer element
457
+ */
458
+ async show(htmlContent, options = {}) {
459
+ // Apply provided options to this instance
460
+ if (options.position) this.position = options.position;
461
+ if (options.maxHeight) this.maxHeight = options.maxHeight;
462
+ if (options.minHeight) this.minHeight = options.minHeight;
463
+
464
+ // Close button visibility
465
+ const pos = this.position || "bottom";
466
+ const defaultShowClose = pos === "left" || pos === "right";
467
+ const showClose = options.showClose === undefined ? defaultShowClose : !!options.showClose;
468
+ this.showClose = showClose;
469
+
470
+ // Render content (header/body)
471
+ await this.setContent(htmlContent, options.header);
472
+
473
+ // Wait for next frame so slots are distributed
474
+ await new Promise((r) => requestAnimationFrame(() => r()));
475
+
476
+ // Optionally wait for media to load (default: true)
477
+ const shouldWaitForMedia = options.waitForMedia !== false;
478
+ if (shouldWaitForMedia) {
479
+ const mediaTimeout = options.mediaTimeout || 500;
480
+ await this.#waitForMedia(mediaTimeout);
481
+ }
482
+
483
+ this.openDrawer();
484
+ return this;
485
+ }
486
+
487
+ /**
488
+ * Set drawer content using slots
489
+ * @param {any|HTMLElement|string} bodyContent - Content for drawer body (HTMLElement or string; Lit templates supported if runtime available)
490
+ * @param {any|HTMLElement|string} headerContent - Optional content for drawer header
491
+ */
492
+ /**
493
+ * Set the content of the drawer
494
+ * @method setContent
495
+ * @public
496
+ * @param {any|HTMLElement|string} bodyContent - Content for the drawer body
497
+ * @param {any|HTMLElement|string} [headerContent] - Optional header content
498
+ * @returns {Promise<void>}
499
+ */
500
+ async setContent(bodyContent, headerContent = null) {
501
+ // Clear existing slotted content
502
+ this.querySelectorAll('[slot="drawer-content"], [slot="drawer-header"]').forEach(el => el.remove());
503
+
504
+ // Add new body content
505
+ if (bodyContent) {
506
+ const bodyWrapper = document.createElement('div');
507
+ bodyWrapper.setAttribute('slot', 'drawer-content');
508
+ //bodyWrapper.className = 'surface-overlay';
509
+
510
+ // Best-effort support for Lit templates only if lit renderer is available at runtime
511
+ if (bodyContent && bodyContent._$litType$) {
512
+ try {
513
+ const mod = await import("#pds/lit");
514
+ mod.render(bodyContent, bodyWrapper);
515
+ } catch {
516
+ // Fallback: attempt to set as text
517
+ bodyWrapper.textContent = String(bodyContent);
518
+ }
519
+ } else if (typeof bodyContent === 'string') {
520
+ bodyWrapper.innerHTML = bodyContent;
521
+ } else {
522
+ bodyWrapper.appendChild(bodyContent);
523
+ }
524
+ this.appendChild(bodyWrapper);
525
+ }
526
+
527
+ // Add new header content
528
+ if (headerContent) {
529
+ const headerWrapper = document.createElement('div');
530
+ headerWrapper.setAttribute('slot', 'drawer-header');
531
+ //headerWrapper.className = 'surface-overlay';
532
+
533
+ if (headerContent && headerContent._$litType$) {
534
+ try {
535
+ const mod = await import("#pds/lit");
536
+ mod.render(headerContent, headerWrapper);
537
+ } catch {
538
+ headerWrapper.textContent = String(headerContent);
539
+ }
540
+ } else if (typeof headerContent === 'string') {
541
+ headerWrapper.innerHTML = headerContent;
542
+ } else {
543
+ headerWrapper.appendChild(headerContent);
544
+ }
545
+ this.appendChild(headerWrapper);
546
+ }
547
+
548
+ // Recalculate height after content is rendered
549
+ // Use double RAF to ensure slots are fully processed
550
+ requestAnimationFrame(() => {
551
+ requestAnimationFrame(() => {
552
+ this.#recalc();
553
+ });
554
+ });
555
+ }
556
+
557
+ /**
558
+ * Clear drawer content (removes all slotted content)
559
+ * @method clearContent
560
+ * @public
561
+ */
562
+ clearContent() {
563
+ this.querySelectorAll('[slot="drawer-content"], [slot="drawer-header"]').forEach(el => el.remove());
564
+ }
565
+
566
+ // Events
567
+ #onBackdropClick = () => this.closeDrawer();
568
+
569
+ #onKeyDown = (e) => {
570
+ if (this.open && e.key === "Escape") this.closeDrawer();
571
+ };
572
+
573
+ #onPointerDown = (e) => {
574
+ if (this._drag === "none") return;
575
+ if (this._drag === "header") {
576
+ const header = this.shadowRoot.querySelector("header");
577
+ const path = e.composedPath();
578
+ if (!path.includes(header)) return;
579
+ }
580
+ const p = this.#getPoint(e);
581
+ this.#isDragging = true;
582
+ this.#startX = p.x;
583
+ this.#startY = p.y;
584
+ this.#lastX = p.x;
585
+ this.#lastY = p.y;
586
+ this.#lastTS = performance.now();
587
+ this.#velocity = 0;
588
+ this.#startFraction = this.#currentFraction;
589
+
590
+ // Capture pointer so dragging continues outside the element
591
+ if (e.target?.setPointerCapture && e.pointerId != null) {
592
+ try {
593
+ e.target.setPointerCapture(e.pointerId);
594
+ } catch { /* */}
595
+ }
596
+
597
+ cancelAnimationFrame(this.#raf);
598
+ this.style.userSelect = "none";
599
+ document.documentElement.style.cursor = "grabbing";
600
+ this.shadowRoot.querySelector("main")?.style.setProperty("overflow", "hidden");
601
+ };
602
+
603
+ #onPointerMove = (e) => {
604
+ if (!this.#isDragging) return;
605
+ const p = this.#getPoint(e);
606
+ const isVertical = this.position === "bottom" || this.position === "top";
607
+ const dir = this.position === "bottom" || this.position === "right" ? 1 : -1;
608
+ const deltaFromStart = isVertical ? (p.y - this.#startY) : (p.x - this.#startX);
609
+ const extent = isVertical ? Math.max(1, this.#drawerHeight) : Math.max(1, this.#drawerWidth);
610
+ const next = this.#clamp(this.#startFraction + (dir * deltaFromStart) / extent, 0, 1);
611
+ this.#applyFraction(next, false);
612
+
613
+ // Velocity (px/ms), positive when moving down in screen coords
614
+ const now = performance.now();
615
+ const dt = Math.max(1, now - this.#lastTS);
616
+ const comp = isVertical ? p.y : p.x;
617
+ const lastComp = isVertical ? this.#lastY : this.#lastX;
618
+ this.#velocity = (comp - lastComp) / dt; // px/ms along active axis
619
+ if (isVertical) this.#lastY = p.y; else this.#lastX = p.x;
620
+ this.#lastTS = now;
621
+
622
+ if (e.cancelable) e.preventDefault();
623
+ };
624
+
625
+ #onPointerUp = (e) => {
626
+ if (!this.#isDragging) return;
627
+ this.#isDragging = false;
628
+ this.style.userSelect = "";
629
+ document.documentElement.style.cursor = "";
630
+ this.shadowRoot.querySelector("main")?.style.removeProperty("overflow");
631
+
632
+ //const isVertical = this.position === "bottom" || this.position === "top";
633
+ const dir = this.position === "bottom" || this.position === "right" ? 1 : -1;
634
+ //const throwCloseThreshold = (1.0 / 1000) * 1000; // keep var for clarity; we use 1.0 px/ms below
635
+
636
+ // Decide based on velocity first (positive in closing direction), else position threshold
637
+ const fastForward = this.#velocity * dir > 1.0; // closing direction
638
+ const fastBackward = this.#velocity * dir < -1.0; // opening direction
639
+
640
+ if (fastForward) {
641
+ this.#animateTo(1); // close
642
+ } else if (fastBackward) {
643
+ this.#animateTo(0); // open
644
+ } else {
645
+ const shouldClose = this.#currentFraction >= 0.5;
646
+ this.#animateTo(shouldClose ? 1 : 0);
647
+ }
648
+
649
+ // Release pointer capture
650
+ if (e.target?.releasePointerCapture && e.pointerId != null) {
651
+ try {
652
+ e.target.releasePointerCapture(e.pointerId);
653
+ } catch {/**/}
654
+ }
655
+ };
656
+
657
+ #recalc = () => {
658
+ if (!this.#aside) return;
659
+ const rect = this.#aside.getBoundingClientRect();
660
+ this.#drawerHeight = rect.height || 0;
661
+ this.#drawerWidth = rect.width || 0;
662
+ this.#applyFraction(this.#currentFraction, false);
663
+ };
664
+
665
+ // Helpers
666
+ #cancelPendingOpenAnimation() {
667
+ if (!this.#openAnimationController) return;
668
+ this.#openAnimationController.abort();
669
+ this.#openAnimationController = null;
670
+ }
671
+
672
+ #queueOpenAnimation() {
673
+ const aside = this.#aside;
674
+ if (!aside) return;
675
+
676
+ this.#cancelPendingOpenAnimation();
677
+ const controller = new AbortController();
678
+ this.#openAnimationController = controller;
679
+
680
+ this.#applyFraction(1, false);
681
+ void aside.offsetHeight; // Force layout to register the closed state
682
+
683
+ this.#whenReadyForOpen(controller.signal)
684
+ .then(() => {
685
+ if (controller.signal.aborted) return;
686
+ this.#animateTo(0);
687
+ })
688
+ .finally(() => {
689
+ if (this.#openAnimationController === controller) {
690
+ this.#openAnimationController = null;
691
+ }
692
+ });
693
+ }
694
+
695
+ async #whenReadyForOpen(signal) {
696
+ await this.#nextFrame(signal);
697
+ await this.#nextFrame(signal);
698
+ await this.#waitForIdle(signal);
699
+ }
700
+
701
+ #nextFrame(signal) {
702
+ if (signal?.aborted) return Promise.resolve();
703
+ return new Promise((resolve) => {
704
+ const id = requestAnimationFrame(() => resolve());
705
+ signal?.addEventListener("abort", () => {
706
+ cancelAnimationFrame(id);
707
+ resolve();
708
+ }, { once: true });
709
+ });
710
+ }
711
+
712
+ #waitForIdle(signal) {
713
+ if (signal?.aborted) return Promise.resolve();
714
+ if (typeof window.requestIdleCallback === "function") {
715
+ return new Promise((resolve) => {
716
+ const idleId = window.requestIdleCallback(() => resolve());
717
+ signal?.addEventListener("abort", () => {
718
+ if (typeof window.cancelIdleCallback === "function") {
719
+ window.cancelIdleCallback(idleId);
720
+ }
721
+ resolve();
722
+ }, { once: true });
723
+ });
724
+ }
725
+ // Fallback: wait for another frame as a lightweight idle approximation
726
+ return this.#nextFrame(signal);
727
+ }
728
+
729
+ async #waitForMedia(maxTimeout = 500) {
730
+ // Find media elements within the drawer (including slotted content)
731
+ const media = Array.from(this.querySelectorAll("img, video"));
732
+ if (media.length === 0) return;
733
+
734
+ const mediaPromises = media.map((el) => {
735
+ if (el.tagName === "IMG") {
736
+ const img = /** @type {HTMLImageElement} */ (el);
737
+ if (img.complete && img.naturalHeight !== 0) return Promise.resolve();
738
+ return new Promise((resolve) => {
739
+ img.addEventListener("load", resolve, { once: true });
740
+ img.addEventListener("error", resolve, { once: true });
741
+ });
742
+ }
743
+ if (el.tagName === "VIDEO") {
744
+ const vid = /** @type {HTMLVideoElement} */ (el);
745
+ if (vid.readyState > 0) return Promise.resolve();
746
+ return new Promise((resolve) => {
747
+ vid.addEventListener("loadedmetadata", resolve, { once: true });
748
+ vid.addEventListener("error", resolve, { once: true });
749
+ });
750
+ }
751
+ return Promise.resolve();
752
+ });
753
+
754
+ const timeout = new Promise((resolve) => setTimeout(resolve, maxTimeout));
755
+ await Promise.race([Promise.all(mediaPromises), timeout]);
756
+ }
757
+ #getPoint(e) {
758
+ if (e.touches && e.touches[0])
759
+ return { x: e.touches[0].clientX, y: e.touches[0].clientY };
760
+ return { x: e.clientX ?? 0, y: e.clientY ?? 0 };
761
+ }
762
+ #clamp(v, lo, hi) {
763
+ return Math.min(hi, Math.max(lo, v));
764
+ }
765
+
766
+ #applyFraction(f, withTransition) {
767
+ this.#currentFraction = this.#clamp(f, 0, 1);
768
+ const t = withTransition ? `transform var(--_dur) var(--_easing)` : "none";
769
+ const aside = this.#aside;
770
+ if (!aside) return;
771
+ aside.style.transition = t;
772
+ if (this._position === "bottom" || this._position === "top") {
773
+ const yPct = this._position === "bottom" ? this.#currentFraction * 100 : -this.#currentFraction * 100;
774
+ aside.style.transform = `translateY(${yPct}%)`;
775
+ } else {
776
+ const xPct = this._position === "right" ? this.#currentFraction * 100 : -this.#currentFraction * 100;
777
+ aside.style.transform = `translateX(${xPct}%)`;
778
+ }
779
+ }
780
+
781
+ // Whether to show the close icon button
782
+ #shouldShowClose() {
783
+ // Always show for side drawers; hide by default for top/bottom unless showClose flag is set
784
+ if (this._position === "left" || this._position === "right") return true;
785
+ if (this._position === "top" || this._position === "bottom") return !!this._showClose;
786
+ return !!this._showClose;
787
+ }
788
+
789
+ #renderCloseButtonVisibility() {
790
+ const btn = this.shadowRoot?.querySelector('.close-btn');
791
+ if (!btn) return;
792
+ btn.hidden = !this.#shouldShowClose();
793
+ if (!btn._pdsBound) {
794
+ btn.addEventListener('click', () => this.closeDrawer());
795
+ btn._pdsBound = true;
796
+ }
797
+ }
798
+
799
+ #animateTo(targetFraction) {
800
+ const aside = this.#aside;
801
+ if (!aside) return;
802
+
803
+ const clamped = this.#clamp(targetFraction, 0, 1);
804
+ const isOpening = clamped < this.#currentFraction;
805
+
806
+ // On mobile, ensure the browser recognizes the starting position before animating
807
+ // This fixes the missing transition when opening
808
+ if (isOpening) {
809
+ // First, ensure we're at the closed position without transition
810
+ aside.style.transition = 'none';
811
+ if (this._position === "bottom" || this._position === "top") {
812
+ const startPct = this._position === "bottom" ? this.#currentFraction * 100 : -this.#currentFraction * 100;
813
+ aside.style.transform = `translateY(${startPct}%)`;
814
+ } else {
815
+ const startPct = this._position === "right" ? this.#currentFraction * 100 : -this.#currentFraction * 100;
816
+ aside.style.transform = `translateX(${startPct}%)`;
817
+ }
818
+ // Force reflow to ensure starting position is applied
819
+ void aside.offsetHeight;
820
+ }
821
+
822
+ // Now apply transition and animate to target
823
+ aside.style.transition = `transform var(--_dur) var(--_easing)`;
824
+ this.#currentFraction = clamped;
825
+ if (this._position === "bottom" || this._position === "top") {
826
+ const yPct = this._position === "bottom" ? clamped * 100 : -clamped * 100;
827
+ aside.style.transform = `translateY(${yPct}%)`;
828
+ } else {
829
+ const xPct = this._position === "right" ? clamped * 100 : -clamped * 100;
830
+ aside.style.transform = `translateX(${xPct}%)`;
831
+ }
832
+
833
+ // Update the `open` property based on the target fraction
834
+ const isOpen = clamped === 0;
835
+ if (this._open !== isOpen) {
836
+ // Avoid recursion: just sync internal and attribute
837
+ this._open = isOpen;
838
+ this.toggleAttribute("open", isOpen);
839
+ this.#syncAria();
840
+ }
841
+ }
842
+
843
+ #syncAria() {
844
+ const layerEl = this.shadowRoot?.getElementById('layer');
845
+ const aside = this.#aside;
846
+ if (layerEl) layerEl.setAttribute('aria-hidden', String(!this._open));
847
+ if (aside) {
848
+ if (this._open) {
849
+ aside.setAttribute('role', 'dialog');
850
+ aside.setAttribute('aria-modal', 'true');
851
+ } else {
852
+ aside.removeAttribute('role');
853
+ aside.removeAttribute('aria-modal');
854
+ }
855
+ }
856
+ }
857
+ })