@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,401 @@
1
+ /**
2
+ * @component pds-splitpanel
3
+ * @description A split panel component that supports horizontal and vertical layouts, resizable panels, and a responsive mobile view.
4
+ *
5
+ * @attr {String} layout - The layout direction of the panels. Can be "horizontal" or "vertical". Defaults to "horizontal".
6
+ * @attr {String} defaultsplit - The initial size of the primary (left/top) panel. Defaults to "450px".
7
+ * @attr {Number} breakpoint - The viewport width in pixels below which the component switches to mobile view. Defaults to 1024.
8
+ * @attr {Boolean} open - Controls the visibility of the primary panel in mobile view.
9
+ *
10
+ * @prop {String} layout - Gets or sets the layout direction.
11
+ * @prop {String} defaultSplit - Gets or sets the default split size.
12
+ * @prop {Number} breakpoint - Gets or sets the mobile breakpoint.
13
+ * @prop {Boolean} open - Gets or sets the open state of the mobile panel.
14
+ *
15
+ * @slot left - Content for the left (or top) panel.
16
+ * @slot right - Content for the right (or bottom) panel.
17
+ *
18
+ * @csspart toggle - The mobile toggle button.
19
+ *
20
+ * @cssprop --left-width - Width of the left panel in horizontal layout.
21
+ * @cssprop --color-border - Color of the splitter bar.
22
+ * @cssprop --color-surface-base - Background color of the left panel in mobile view.
23
+ * @cssprop --transition-fast - Transition duration for the mobile panel animation.
24
+ * @cssprop --spacing-4 - Positioning spacing for the mobile toggle button.
25
+ * @cssprop --spacing-1 - Padding for the mobile toggle button.
26
+ * @cssprop --spacing-2 - Padding for the mobile toggle button.
27
+ *
28
+ * @example
29
+ * <caption>Basic horizontal split</caption>
30
+ * <pds-splitpanel>
31
+ * <div slot="left">Left Panel Content</div>
32
+ * <div slot="right">Right Panel Content</div>
33
+ * </pds-splitpanel>
34
+ *
35
+ * @example
36
+ * <caption>Vertical split with custom default size</caption>
37
+ * <pds-splitpanel layout="vertical" defaultsplit="200px">
38
+ * <div slot="left">Top Panel Content</div>
39
+ * <div slot="right">Bottom Panel Content</div>
40
+ * </pds-splitpanel>
41
+ */
42
+ customElements.define(
43
+ "pds-splitpanel",
44
+ class extends HTMLElement {
45
+ static get observedAttributes() {
46
+ return ["layout", "defaultsplit", "breakpoint", "open"];
47
+ }
48
+
49
+ constructor() {
50
+ super();
51
+ this.attachShadow({ mode: "open" });
52
+
53
+ // Defaults
54
+ this._layout = "horizontal";
55
+ this._defaultSplit = "450px";
56
+ this._breakpoint = 1024;
57
+ this._open = this.hasAttribute("open");
58
+ this.isDragging = false;
59
+
60
+ // Bound handlers for add/remove
61
+ this._onResize = () => this.updateLayout();
62
+ this._onMouseMove = (e) => this.drag(e);
63
+ this._onMouseUp = () => this.stopDragging();
64
+
65
+ this._render();
66
+ }
67
+
68
+ connectedCallback() {
69
+ // Ensure defaults reflected if attributes missing
70
+ if (!this.hasAttribute("layout")) this.setAttribute("layout", this._layout);
71
+ if (!this.hasAttribute("defaultsplit")) this.setAttribute("defaultsplit", this._defaultSplit);
72
+ if (!this.hasAttribute("breakpoint")) this.setAttribute("breakpoint", String(this._breakpoint));
73
+ if (this._open) this.setAttribute("open", "");
74
+
75
+ // Adopt primitives + component stylesheet (fallback to <style> if PDS not present)
76
+ this._adoptStyles();
77
+
78
+ // Cache references
79
+ this.$leftWrap = this.shadowRoot.querySelector(".left-panel");
80
+ this.$rightWrap = this.shadowRoot.querySelector(".right-panel");
81
+ this.$splitter = this.shadowRoot.querySelector(".splitter");
82
+ this.$toggleBtn = this.shadowRoot.getElementById("mobile-toggle");
83
+ this.$overlay = this.shadowRoot.querySelector(".mobile-overlay");
84
+ this.$icon = this.shadowRoot.querySelector("pds-icon");
85
+
86
+ // Wire events
87
+ window.addEventListener("resize", this._onResize);
88
+ if (this.$splitter) this.$splitter.addEventListener("mousedown", (e) => this.startDragging(e));
89
+ document.addEventListener("mousemove", this._onMouseMove);
90
+ document.addEventListener("mouseup", this._onMouseUp);
91
+ if (this.$toggleBtn) this.$toggleBtn.addEventListener("click", () => this.toggleMobileView());
92
+ if (this.$overlay) this.$overlay.addEventListener("click", () => this.closeMobileView());
93
+
94
+ // Initial layout
95
+ this.updateLayout();
96
+ this._updateToggleButton();
97
+ }
98
+
99
+ disconnectedCallback() {
100
+ window.removeEventListener("resize", this._onResize);
101
+ document.removeEventListener("mousemove", this._onMouseMove);
102
+ document.removeEventListener("mouseup", this._onMouseUp);
103
+ if (this.$splitter) this.$splitter.removeEventListener("mousedown", (e) => this.startDragging(e));
104
+ if (this.$toggleBtn) this.$toggleBtn.removeEventListener("click", () => this.toggleMobileView());
105
+ if (this.$overlay) this.$overlay.removeEventListener("click", () => this.closeMobileView());
106
+ }
107
+
108
+ attributeChangedCallback(name, _oldVal, newVal) {
109
+ switch (name) {
110
+ case "layout":
111
+ this._layout = (newVal || "horizontal").toLowerCase();
112
+ this.updateLayout();
113
+ break;
114
+ case "defaultsplit":
115
+ this._defaultSplit = newVal || "450px";
116
+ this.updateLayout();
117
+ break;
118
+ case "breakpoint":
119
+ this._breakpoint = Number(newVal) || 1024;
120
+ this.updateLayout();
121
+ break;
122
+ case "open":
123
+ this._open = this.hasAttribute("open");
124
+ this._updateToggleButton();
125
+ break;
126
+ }
127
+ }
128
+
129
+ // Properties
130
+ get layout() {
131
+ return this._layout;
132
+ }
133
+ set layout(v) {
134
+ this.setAttribute("layout", v);
135
+ }
136
+
137
+ get defaultSplit() {
138
+ return this._defaultSplit;
139
+ }
140
+ set defaultSplit(v) {
141
+ this.setAttribute("defaultsplit", v);
142
+ }
143
+
144
+ get breakpoint() {
145
+ return this._breakpoint;
146
+ }
147
+ set breakpoint(v) {
148
+ this.setAttribute("breakpoint", String(v));
149
+ }
150
+
151
+ get open() {
152
+ return this._open;
153
+ }
154
+ set open(v) {
155
+ if (Boolean(v)) {
156
+ if (!this.hasAttribute("open")) this.setAttribute("open", "");
157
+ } else {
158
+ if (this.hasAttribute("open")) this.removeAttribute("open");
159
+ }
160
+ }
161
+
162
+ // Rendering
163
+ _render() {
164
+ this.shadowRoot.innerHTML = `
165
+ <div class="left-panel">
166
+ <slot name="left"></slot>
167
+ </div>
168
+ <div class="splitter"></div>
169
+ <div class="right-panel">
170
+ <slot name="right"></slot>
171
+ </div>
172
+ <button part="toggle"
173
+ id="mobile-toggle"
174
+ class="mobile-toggle btn btn-sm"
175
+ aria-label="Toggle panel"
176
+ aria-expanded="${this._open ? "true" : "false"}"
177
+ >
178
+ <pds-icon icon="${this._open ? "x" : "list"}" width="24" height="24"></pds-icon>
179
+ </button>
180
+ <div class="mobile-overlay"></div>
181
+ `;
182
+ }
183
+
184
+ async _adoptStyles() {
185
+ const cssText = `
186
+ :host {
187
+ display: flex;
188
+ position: relative;
189
+ height: 100%;
190
+ width: 100%;
191
+ }
192
+
193
+ :host([layout="horizontal"]) {
194
+ flex-direction: row;
195
+ }
196
+
197
+ :host([layout="vertical"]) {
198
+ flex-direction: column;
199
+ }
200
+
201
+ .left-panel,
202
+ .right-panel {
203
+ display: block;
204
+ min-width: 0;
205
+ }
206
+
207
+ .left-panel {
208
+ flex: 0 0 var(--left-width, 450px);
209
+ }
210
+
211
+ .right-panel {
212
+ flex: 1 1 auto;
213
+ display: flex;
214
+ }
215
+
216
+ .splitter {
217
+ background-color: var(--color-border);
218
+ cursor: col-resize;
219
+ position: relative;
220
+ z-index: 1;
221
+ }
222
+
223
+ :host([layout="horizontal"]) .splitter {
224
+ width: 5px;
225
+ height: 100%;
226
+ }
227
+
228
+ :host([layout="vertical"]) .splitter {
229
+ height: 5px;
230
+ width: 100%;
231
+ cursor: row-resize;
232
+ }
233
+
234
+ #mobile-toggle {
235
+ visibility: hidden;
236
+ position: fixed;
237
+ top: var(--spacing-4);
238
+ right: var(--spacing-4);
239
+ z-index: 1001;
240
+ }
241
+
242
+ :host([mobile]) #mobile-toggle {
243
+ visibility: visible;
244
+ }
245
+
246
+ :host([mobile]) .left-panel {
247
+ display: none;
248
+ }
249
+
250
+ :host([mobile]) .right-panel {
251
+ flex: 1 1 100%;
252
+ width: 100%;
253
+ }
254
+
255
+ :host([mobile][open]) .left-panel {
256
+ display: block;
257
+ position: absolute;
258
+ top: 0;
259
+ left: 0;
260
+ width: 100%;
261
+ height: 100%;
262
+ z-index: 1000;
263
+ background: var(--color-surface-base);
264
+ }
265
+
266
+ :host([mobile]) .left-panel ::slotted([slot="left"]) {
267
+ display: block;
268
+ width: 100%;
269
+ height: 100%;
270
+ transform: translateX(-100%);
271
+ transition: transform var(--transition-fast) ease-in-out;
272
+ }
273
+
274
+ :host([mobile][open]) .left-panel ::slotted([slot="left"]) {
275
+ transform: translateX(0);
276
+ }
277
+
278
+ .mobile-overlay {
279
+ display: none;
280
+ }
281
+
282
+ :host([mobile][open]) .mobile-overlay {
283
+ display: block;
284
+ position: absolute;
285
+ top: 0;
286
+ left: 0;
287
+ width: 100%;
288
+ height: 100%;
289
+ background: rgba(0,0,0,0.5);
290
+ z-index: 999;
291
+ }
292
+
293
+ .mobile-toggle.btn {
294
+ padding: var(--spacing-1, 4px) var(--spacing-2, 6px);
295
+ }
296
+
297
+ .mobile-toggle.btn-sm {
298
+ padding: var(--spacing-1, 4px);
299
+ }
300
+ `;
301
+
302
+ // Prefer PDS layers if available
303
+ try {
304
+ if (window.PDS && typeof PDS.createStylesheet === "function" && typeof PDS.adoptLayers === "function") {
305
+ const componentStyles = PDS.createStylesheet(cssText);
306
+ await PDS.adoptLayers(this.shadowRoot, ["primitives", "components"], [componentStyles]);
307
+ return;
308
+ }
309
+ } catch (e) {
310
+ console.error("pds-splitpanel: adoptLayers failed", e);
311
+ }
312
+
313
+ // Fallback: inject <style> into shadow root
314
+ const style = document.createElement("style");
315
+ style.textContent = cssText;
316
+ this.shadowRoot.prepend(style);
317
+ }
318
+
319
+ /**
320
+ * Updates the layout based on the current viewport width and breakpoint.
321
+ * Toggles mobile mode and adjusts panel styles.
322
+ */
323
+ updateLayout() {
324
+ const isMobile = window.innerWidth < this._breakpoint;
325
+ this.toggleAttribute("mobile", isMobile);
326
+
327
+ if (isMobile) {
328
+ if (!this.hasAttribute("open")) this._open = false;
329
+ if (this.$splitter) this.$splitter.style.display = "none";
330
+ if (this.$rightWrap) this.$rightWrap.style.display = "block";
331
+ this.style.removeProperty("--left-width");
332
+ if (this.$leftWrap) this.$leftWrap.style.height = "";
333
+ if (this.$rightWrap) this.$rightWrap.style.flex = "1 1 auto";
334
+ } else {
335
+ if (this.$splitter) this.$splitter.style.display = "block";
336
+ this.style.setProperty("--left-width", this._defaultSplit);
337
+ if (this.$rightWrap)
338
+ this.$rightWrap.style.flex = `1 1 calc(100% - ${this._defaultSplit})`;
339
+ if (this._layout === "vertical") {
340
+ // For vertical layout, width var isn't used; ensure panels reset
341
+ if (this.$leftWrap) this.$leftWrap.style.height = "";
342
+ if (this.$rightWrap) this.$rightWrap.style.flex = "1 1 auto";
343
+ }
344
+ }
345
+ this._updateToggleButton();
346
+ }
347
+
348
+ startDragging(event) {
349
+ if (this.hasAttribute("mobile")) return;
350
+ this.isDragging = true;
351
+ document.body.style.cursor =
352
+ this._layout === "horizontal" ? "col-resize" : "row-resize";
353
+ event.preventDefault();
354
+ }
355
+
356
+ drag(event) {
357
+ if (!this.isDragging) return;
358
+
359
+ const newSize =
360
+ this._layout === "horizontal"
361
+ ? Math.max(200, Math.min(event.clientX, window.innerWidth - 200))
362
+ : Math.max(200, Math.min(event.clientY, window.innerHeight - 200));
363
+
364
+ if (this._layout === "horizontal") {
365
+ this.style.setProperty("--left-width", `${newSize}px`);
366
+ if (this.$rightWrap)
367
+ this.$rightWrap.style.flex = `1 1 calc(100% - ${newSize}px)`;
368
+ } else {
369
+ if (this.$leftWrap) this.$leftWrap.style.height = `${newSize}px`;
370
+ if (this.$rightWrap)
371
+ this.$rightWrap.style.flex = `1 1 calc(100% - ${newSize}px)`;
372
+ }
373
+ }
374
+
375
+ stopDragging() {
376
+ if (!this.isDragging) return;
377
+ this.isDragging = false;
378
+ document.body.style.cursor = "";
379
+ }
380
+
381
+ /**
382
+ * Toggles the visibility of the primary panel in mobile view.
383
+ */
384
+ toggleMobileView() {
385
+ this.open = !this._open;
386
+ }
387
+
388
+ /**
389
+ * Closes the primary panel in mobile view.
390
+ */
391
+ closeMobileView() {
392
+ this.open = false;
393
+ }
394
+
395
+ _updateToggleButton() {
396
+ if (!this.$toggleBtn || !this.$icon) return;
397
+ this.$toggleBtn.setAttribute("aria-expanded", this._open ? "true" : "false");
398
+ this.$icon.setAttribute("icon", this._open ? "x" : "list");
399
+ }
400
+ }
401
+ );
@@ -0,0 +1,251 @@
1
+ /**
2
+ * @element pds-tabpanel
3
+ *
4
+ * @attr {string} label - Label for the tab button
5
+ * @attr {string} id - Unique identifier for the panel (auto-generated if not provided)
6
+ *
7
+ * @slot - Content of the tab panel
8
+ */
9
+ class TabPanel extends HTMLElement {
10
+ connectedCallback() {
11
+ // ensure id + label
12
+ if (!this.id) this.id = `tab-${Math.random().toString(36).slice(2, 7)}`;
13
+ if (!this.hasAttribute("label")) this.setAttribute("label", this.id);
14
+
15
+ // Wrap light DOM into a <section> once (idempotent)
16
+ if (!this._section) {
17
+ const section = document.createElement("section");
18
+ section.setAttribute("role", "region");
19
+ section.id = this.id;
20
+ section.setAttribute("aria-label", this.getAttribute("label") || this.id);
21
+ section.dataset.tabpanel = "";
22
+ // Move children into section
23
+ while (this.firstChild) section.appendChild(this.firstChild);
24
+ this.appendChild(section);
25
+ this._section = section;
26
+ }
27
+ }
28
+ /**
29
+ * The inner `<section>` that exposes the panel region semantics.
30
+ * @returns {HTMLElement|null}
31
+ */
32
+ get section() {
33
+ return this.querySelector("[data-tabpanel]");
34
+ }
35
+ }
36
+ customElements.define("pds-tabpanel", TabPanel);
37
+
38
+ /**
39
+ * Tab navigation component that pairs anchors with `pds-tabpanel` children.
40
+ *
41
+ * @element pds-tabstrip
42
+ * @fires tabchange - Fired when the active tab changes. Detail: `{ oldTab: string|null, newTab: string }`
43
+ *
44
+ * @attr {string} label - Accessible label announced for the tablist
45
+ * @attr {string} selected - Identifier of the currently active panel (synced with the location hash)
46
+ *
47
+ * @slot - Collection of `pds-tabpanel` nodes representing individual tab panels
48
+ *
49
+ * @csspart tabs - Navigation container comprising the clickable tab buttons
50
+ * @cssprop --color-accent-400 - Color of the active tab indicator underline
51
+ */
52
+ class TabStrip extends HTMLElement {
53
+ #shadow = this.attachShadow({ mode: "open" });
54
+ #inkbar;
55
+ #panels = [];
56
+ #mo;
57
+ #currentTab = null;
58
+
59
+ constructor() {
60
+ super();
61
+ this.#shadow.innerHTML = /*html*/ `
62
+ <style>
63
+ :host{display:block}
64
+ nav{
65
+ position:relative; display:inline-flex; gap:.5rem; align-items:flex-end;
66
+
67
+ --pad-x:.5rem; --pad-y:.25rem;
68
+ }
69
+ nav a{
70
+ color: currentColor;
71
+ display:inline-block; padding:var(--pad-y) var(--pad-x);
72
+ text-decoration:none; line-height:1.2; border-bottom:2px solid transparent;
73
+ cursor:pointer;
74
+ }
75
+ nav a[aria-current="page"]{ font-weight:600; }
76
+ nav a:focus-visible{ outline:auto; outline-offset:2px; }
77
+ .inkbar{
78
+ position:absolute; inset-inline-start:0; bottom:-1px; height:2px; width:0;
79
+ transform:translateX(0); transition:transform .25s ease, width .25s ease;
80
+ background-color: var(--color-accent-400); pointer-events:none;
81
+ }
82
+ </style>
83
+ <nav part="tabs"></nav>
84
+ <slot></slot>
85
+ `;
86
+ }
87
+
88
+ /**
89
+ * Attach event listeners and observe panels once connected.
90
+ */
91
+ connectedCallback() {
92
+ // Set nav aria-label based on attribute or default
93
+ const nav = this.#shadow.querySelector("nav");
94
+ nav.setAttribute("aria-label", this.getAttribute("label") || "Tabs");
95
+
96
+ // Build once panels are in the light DOM
97
+ queueMicrotask(() => {
98
+ this.#collectPanels();
99
+ this.#renderTabs();
100
+ this.#syncFromUrl(true);
101
+ this.#positionInkbar();
102
+ });
103
+
104
+ // Observe changes to children/attributes that affect tabs
105
+ this.#mo = new MutationObserver(() => {
106
+ this.#collectPanels();
107
+ this.#renderTabs();
108
+ this.#syncFromUrl(false);
109
+ this.#positionInkbar();
110
+ });
111
+ this.#mo.observe(this, {
112
+ subtree: true,
113
+ childList: true,
114
+ attributes: true,
115
+ attributeFilter: ["id", "label"],
116
+ });
117
+
118
+ // Respond to URL + layout changes
119
+ addEventListener("hashchange", this.#onUrlChange, { passive: true });
120
+ addEventListener("popstate", this.#onUrlChange, { passive: true });
121
+ addEventListener("resize", this.#positionInkbar, { passive: true });
122
+
123
+ // Handle clicks/keys in the shadow nav
124
+ nav.addEventListener("click", this.#onNavClick);
125
+ nav.addEventListener("keydown", this.#onNavKeydown);
126
+ }
127
+
128
+ // --- helpers ---
129
+ #collectPanels() {
130
+ // Direct children <pds-tabpanel> only (your structure)
131
+ this.#panels = Array.from(this.querySelectorAll(":scope > pds-tabpanel"));
132
+ // Ensure each has an id + section
133
+ this.#panels.forEach((p, i) => {
134
+ if (!p.id) p.id = `tab-${i + 1}`;
135
+ p.connectedCallback?.(); // make sure its section exists
136
+ });
137
+ }
138
+
139
+ #renderTabs() {
140
+ const nav = this.#shadow.querySelector("nav");
141
+ nav.innerHTML =
142
+ this.#panels
143
+ .map((p) => {
144
+ const id = p.id;
145
+ const label = p.getAttribute("label") || id;
146
+ return `<a href="#${id}" aria-controls="${id}">${label}</a>`;
147
+ })
148
+ .join("") + `<span class="inkbar" aria-hidden="true"></span>`;
149
+ this.#inkbar = nav.querySelector(".inkbar");
150
+ }
151
+
152
+ #syncFromUrl(initial = false) {
153
+ if (!this.#panels.length) return;
154
+ const hashId = (location.hash || "").slice(1);
155
+ const exists = this.#panels.some((p) => p.id === hashId);
156
+ const next = exists
157
+ ? hashId
158
+ : this.getAttribute("selected") || this.#panels[0].id;
159
+
160
+ // Track previous tab for event
161
+ const oldTab = this.#currentTab;
162
+ const changed = oldTab !== null && oldTab !== next;
163
+
164
+ // Update selected attribute (optional external reflection)
165
+ this.setAttribute("selected", next);
166
+ this.#currentTab = next;
167
+
168
+ // Show/hide panels
169
+ for (const p of this.#panels) {
170
+ const sec = p.section || p.querySelector("[data-tabpanel]");
171
+ if (!sec) continue;
172
+ const active = p.id === next;
173
+ sec.hidden = !active; // semantic
174
+ sec.setAttribute("aria-hidden", String(!active));
175
+ sec.style.display = active ? "" : "none"; // guaranteed visual hide
176
+ if (active) sec.setAttribute("tabindex", "0");
177
+ else sec.removeAttribute("tabindex");
178
+ }
179
+
180
+ // Mark active link
181
+ const links = this.#shadow.querySelectorAll('nav a[href^="#"]');
182
+ links.forEach((a) => {
183
+ const target = a.getAttribute("href").slice(1);
184
+ if (target === next) a.setAttribute("aria-current", "page");
185
+ else a.removeAttribute("aria-current");
186
+ });
187
+
188
+ this.#positionInkbar();
189
+
190
+ // Stabilize URL on first paint if needed
191
+ if (initial && (!hashId || !exists)) {
192
+ history.replaceState(null, "", `#${next}`);
193
+ }
194
+
195
+ // Emit tabchange event (skip initial load)
196
+ if (changed) {
197
+ this.dispatchEvent(
198
+ new CustomEvent("tabchange", {
199
+ bubbles: true,
200
+ composed: true,
201
+ detail: { oldTab, newTab: next }
202
+ })
203
+ );
204
+ }
205
+ }
206
+
207
+ #positionInkbar = () => {
208
+ const nav = this.#shadow.querySelector("nav");
209
+ if (!nav) return;
210
+ const active = nav.querySelector('a[aria-current="page"]');
211
+ if (!active) return;
212
+ const nb = nav.getBoundingClientRect();
213
+ const ab = active.getBoundingClientRect();
214
+ const w = Math.max(0, ab.width);
215
+ const x = Math.max(0, ab.left - nb.left);
216
+ this.#inkbar.style.width = `${w}px`;
217
+ this.#inkbar.style.transform = `translateX(${x}px)`;
218
+ };
219
+
220
+ #onUrlChange = () => this.#syncFromUrl(false);
221
+
222
+ #onNavClick = (e) => {
223
+ const a = e
224
+ .composedPath()
225
+ .find(
226
+ (el) => el?.tagName === "A" && el.getAttribute("href")?.startsWith("#")
227
+ );
228
+ if (!a) return;
229
+ e.preventDefault(); // prevent anchor scroll/jump
230
+ const id = a.getAttribute("href").slice(1);
231
+ if (!id) return;
232
+ if (location.hash.slice(1) !== id) history.pushState(null, "", `#${id}`);
233
+ this.#syncFromUrl(false);
234
+ };
235
+
236
+ #onNavKeydown = (e) => {
237
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
238
+ const links = Array.from(this.#shadow.querySelectorAll('nav a[href^="#"]'));
239
+ const i = links.indexOf(
240
+ this.#shadow.activeElement || document.activeElement
241
+ );
242
+ if (i === -1) return;
243
+ e.preventDefault();
244
+ const next =
245
+ e.key === "ArrowRight"
246
+ ? links[(i + 1) % links.length]
247
+ : links[(i - 1 + links.length) % links.length];
248
+ next?.focus();
249
+ };
250
+ }
251
+ customElements.define("pds-tabstrip", TabStrip);