@pure-ds/storybook 0.7.25 → 0.7.26

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 (68) hide show
  1. package/.storybook/preview.js +5 -4
  2. package/dist/pds-reference.json +2266 -1017
  3. package/package.json +2 -2
  4. package/public/assets/js/app.js +1 -1
  5. package/public/assets/js/pds-ask.js +6 -6
  6. package/public/assets/js/pds-manager.js +104 -29
  7. package/public/assets/pds/components/pds-calendar.js +91 -159
  8. package/public/assets/pds/components/pds-daterange.js +683 -0
  9. package/public/assets/pds/components/pds-form.js +123 -21
  10. package/public/assets/pds/components/pds-rating.js +648 -0
  11. package/public/assets/pds/components/pds-tags.js +802 -0
  12. package/public/assets/pds/core/pds-ask.js +6 -6
  13. package/public/assets/pds/core/pds-manager.js +104 -29
  14. package/public/assets/pds/custom-elements.json +1099 -74
  15. package/public/assets/pds/pds-css-complete.json +7 -2
  16. package/public/assets/pds/pds-runtime-config.json +1 -1
  17. package/public/assets/pds/pds.css-data.json +4 -4
  18. package/public/assets/pds/styles/pds-components.css +96 -24
  19. package/public/assets/pds/styles/pds-components.css.js +192 -48
  20. package/public/assets/pds/styles/pds-primitives.css +6 -3
  21. package/public/assets/pds/styles/pds-primitives.css.js +12 -6
  22. package/public/assets/pds/styles/pds-styles.css +104 -29
  23. package/public/assets/pds/styles/pds-styles.css.js +208 -58
  24. package/public/assets/pds/styles/pds-utilities.css +2 -2
  25. package/public/assets/pds/styles/pds-utilities.css.js +4 -4
  26. package/public/assets/pds/vscode-custom-data.json +97 -0
  27. package/src/js/common/ask.js +122 -12
  28. package/src/js/pds-core/pds-generator.js +103 -28
  29. package/src/js/pds-core/pds-ontology.js +2 -2
  30. package/stories/components/PdsCalendar.stories.js +650 -168
  31. package/stories/components/PdsDaterange.stories.js +85 -0
  32. package/stories/components/PdsForm.Basics.stories.js +16 -0
  33. package/stories/components/PdsForm.ConditionalAndCalculated.stories.js +16 -0
  34. package/stories/components/PdsForm.CustomContent.stories.js +10 -0
  35. package/stories/components/PdsForm.Dialogs.stories.js +7 -0
  36. package/stories/components/PdsForm.Layout.stories.js +16 -0
  37. package/stories/components/PdsForm.SelectionAndArrays.stories.js +12 -0
  38. package/stories/components/PdsForm.stories.js +179 -219
  39. package/stories/components/PdsIcon.stories.js +17 -7
  40. package/stories/components/PdsOmnibox.stories.js +1 -60
  41. package/stories/components/PdsRating.stories.js +126 -0
  42. package/stories/components/PdsTags.stories.js +224 -0
  43. package/stories/components/PdsToaster.stories.js +1 -1
  44. package/stories/components/omnibox-countries-api-settings.js +63 -0
  45. package/stories/foundations/Colors.stories.js +6 -1
  46. package/stories/foundations/HTMLDefaults.stories.js +6 -6
  47. package/stories/foundations/Icons.stories.js +4 -4
  48. package/stories/foundations/MeshGradients.stories.js +13 -3
  49. package/stories/foundations/SmartSurfaces.stories.js +43 -31
  50. package/stories/foundations/Typography.stories.js +30 -108
  51. package/stories/foundations/ZIndex.stories.js +3 -3
  52. package/stories/layout/LayoutOverview.stories.js +3 -3
  53. package/stories/layout/LayoutSystem.stories.js +24 -18
  54. package/stories/patterns/BorderEffects.stories.js +28 -16
  55. package/stories/patterns/InteractiveStates.stories.js +39 -24
  56. package/stories/patterns/Utilities.stories.js +45 -23
  57. package/stories/primitives/ArticleLayout.stories.js +176 -0
  58. package/stories/primitives/Badges.stories.js +24 -8
  59. package/stories/primitives/Buttons.stories.js +29 -9
  60. package/stories/primitives/Callouts.stories.js +8 -7
  61. package/stories/primitives/Cards.stories.js +4 -1
  62. package/stories/primitives/FormElements.stories.js +8 -2
  63. package/stories/primitives/HtmlFormElements.stories.js +8 -2
  64. package/stories/primitives/HtmlFormGroups.stories.js +5 -2
  65. package/stories/primitives/Media.stories.js +44 -30
  66. package/stories/primitives/Tables.stories.js +25 -7
  67. package/stories/utils/PdsAsk.stories.js +17 -5
  68. package/stories/utils/PdsObjectApi.stories.js +16 -4
@@ -1,276 +1,586 @@
1
- import { html } from 'lit';
1
+ import { html } from "lit";
2
+ import { PDS } from "#pds";
2
3
 
3
4
  const docsParameters = {
4
5
  description: {
5
- component: 'A fully featured calendar component with month navigation, event display, and expandable day views'
6
- }
6
+ component:
7
+ "A fully featured calendar component with month navigation, event display, and keyboard-accessible date selection",
8
+ },
7
9
  };
8
10
 
9
- if (typeof window !== 'undefined') {
10
- import('../reference/reference-docs.js')
11
+ if (typeof window !== "undefined") {
12
+ import("../reference/reference-docs.js")
11
13
  .then(({ createComponentDocsPage }) => {
12
- docsParameters.page = createComponentDocsPage('pds-calendar');
14
+ docsParameters.page = createComponentDocsPage("pds-calendar");
13
15
  })
14
16
  .catch((error) => {
15
- console.warn('storybook: docs page failed to load for pds-calendar', error);
17
+ console.warn(
18
+ "storybook: docs page failed to load for pds-calendar",
19
+ error,
20
+ );
16
21
  });
17
22
  }
18
23
 
19
24
  export default {
20
- title: 'Components/pds-calendar',
21
- tags: ['autodocs', 'calendar', 'date', 'datepicker', 'event', 'schedule'],
25
+ title: "Components/pds-calendar",
26
+ tags: ["autodocs", "calendar", "date", "datepicker", "event", "schedule"],
22
27
  parameters: {
23
28
  pds: {
24
- tags: ['calendar', 'date', 'datepicker', 'event', 'schedule', 'pds-calendar']
29
+ tags: [
30
+ "calendar",
31
+ "date",
32
+ "datepicker",
33
+ "event",
34
+ "schedule",
35
+ "pds-calendar",
36
+ ],
25
37
  },
26
- docs: docsParameters
38
+ docs: docsParameters,
27
39
  },
28
40
  argTypes: {
29
41
  date: {
30
- control: 'text',
31
- description: 'The date to display (defaults to current date). Accepts any valid date string'
32
- }
33
- }
42
+ control: "text",
43
+ description:
44
+ "The date to display (defaults to current date). Accepts any valid date string",
45
+ },
46
+ },
34
47
  };
35
48
 
36
49
  // Sample event data generator
37
50
  const getEventData = () => ({
38
51
  5: [
39
- { title: 'Team Standup', type: 'primary' },
40
- { title: 'Code Review Session', type: 'info' }
41
- ],
42
- 12: [
43
- { title: 'Sprint Planning', type: 'primary' }
52
+ { title: "Team Standup", type: "primary" },
53
+ { title: "Code Review Session", type: "info" },
44
54
  ],
55
+ 12: [{ title: "Sprint Planning", type: "primary" }],
45
56
  15: [
46
- { title: 'Client Meeting', type: 'warning' },
47
- { title: 'Design Review', type: 'info' }
48
- ],
49
- 18: [
50
- { title: 'Deployment Window', type: 'danger' }
57
+ { title: "Client Meeting", type: "warning" },
58
+ { title: "Design Review", type: "info" },
51
59
  ],
60
+ 18: [{ title: "Deployment Window", type: "danger" }],
52
61
  20: [
53
- { title: 'Team Retrospective', type: 'primary' },
54
- { title: 'Documentation Review', type: 'info' }
55
- ],
56
- 25: [
57
- { title: 'Holiday Event', type: 'warning' }
62
+ { title: "Team Retrospective", type: "primary" },
63
+ { title: "Documentation Review", type: "info" },
58
64
  ],
65
+ 25: [{ title: "Holiday Event", type: "warning" }],
59
66
  28: [
60
- { title: 'Release Candidate Review', type: 'danger' },
61
- { title: 'Performance Testing', type: 'info' }
62
- ]
67
+ { title: "Release Candidate Review", type: "danger" },
68
+ { title: "Performance Testing", type: "info" },
69
+ ],
63
70
  });
64
71
 
65
72
  export const Default = {
66
73
  render: (args) => {
67
74
  setTimeout(() => {
68
- const calendar = document.querySelector('#default-calendar');
75
+ const calendar = document.querySelector("#default-calendar");
69
76
  if (calendar) {
70
- calendar.addEventListener('month-rendered', (e) => {
77
+ calendar.addEventListener("month-rendered", (e) => {
71
78
  e.detail.fill(getEventData());
72
79
  });
73
80
  }
74
81
  }, 0);
75
-
76
- return html`
77
- <pds-calendar id="default-calendar"></pds-calendar>
78
- `;
82
+
83
+ return html` <pds-calendar id="default-calendar"></pds-calendar> `;
79
84
  },
80
85
  args: {
81
- date: ''
82
- }
86
+ date: "",
87
+ },
83
88
  };
84
89
 
85
90
  export const WithSpecificDate = {
86
91
  render: () => {
87
92
  setTimeout(() => {
88
- const calendar = document.querySelector('#specific-date-calendar');
93
+ const calendar = document.querySelector("#specific-date-calendar");
89
94
  if (calendar) {
90
- calendar.addEventListener('month-rendered', (e) => {
95
+ calendar.addEventListener("month-rendered", (e) => {
91
96
  e.detail.fill({
92
- 10: [
93
- { title: 'Important Meeting', type: 'danger' }
94
- ],
95
- 15: [
96
- { title: 'Project Kickoff', type: 'primary' }
97
- ],
98
- 25: [
99
- { title: 'Holiday', type: 'warning' }
100
- ]
97
+ 10: [{ title: "Important Meeting", type: "danger" }],
98
+ 15: [{ title: "Project Kickoff", type: "primary" }],
99
+ 25: [{ title: "Holiday", type: "warning" }],
101
100
  });
102
101
  });
103
102
  }
104
103
  }, 0);
105
-
104
+
106
105
  return html`
107
- <pds-calendar id="specific-date-calendar" date="2024-12-01"></pds-calendar>
106
+ <pds-calendar
107
+ id="specific-date-calendar"
108
+ date="2024-12-01"
109
+ ></pds-calendar>
108
110
  `;
109
- }
111
+ },
110
112
  };
111
113
 
112
114
  export const WithManyEvents = {
113
115
  render: () => {
114
116
  setTimeout(() => {
115
- const calendar = document.querySelector('#many-events-calendar');
117
+ const calendar = document.querySelector("#many-events-calendar");
116
118
  if (calendar) {
117
- calendar.addEventListener('month-rendered', (e) => {
119
+ calendar.addEventListener("month-rendered", (e) => {
118
120
  const events = {};
119
-
121
+
120
122
  // Generate events for every other day
121
123
  for (let i = 1; i <= 30; i += 2) {
122
- const types = ['primary', 'info', 'warning', 'danger'];
124
+ const types = ["primary", "info", "warning", "danger"];
123
125
  const eventCount = Math.floor(Math.random() * 3) + 1;
124
126
  events[i] = [];
125
-
127
+
126
128
  for (let j = 0; j < eventCount; j++) {
127
129
  events[i].push({
128
130
  title: `Event ${j + 1} on Day ${i}`,
129
- type: types[Math.floor(Math.random() * types.length)]
131
+ type: types[Math.floor(Math.random() * types.length)],
130
132
  });
131
133
  }
132
134
  }
133
-
135
+
134
136
  e.detail.fill(events);
135
137
  });
136
138
  }
137
139
  }, 0);
138
-
139
- return html`
140
- <pds-calendar id="many-events-calendar"></pds-calendar>
141
- `;
142
- }
140
+
141
+ return html` <pds-calendar id="many-events-calendar"></pds-calendar> `;
142
+ },
143
143
  };
144
144
 
145
145
  export const EventTypes = {
146
146
  render: () => {
147
147
  setTimeout(() => {
148
- const calendar = document.querySelector('#event-types-calendar');
148
+ const calendar = document.querySelector("#event-types-calendar");
149
149
  if (calendar) {
150
- calendar.addEventListener('month-rendered', (e) => {
150
+ calendar.addEventListener("month-rendered", (e) => {
151
151
  e.detail.fill({
152
- 5: [
153
- { title: 'Primary Event', type: 'primary' }
154
- ],
155
- 10: [
156
- { title: 'Info Event', type: 'info' }
157
- ],
158
- 15: [
159
- { title: 'Warning Event', type: 'warning' }
160
- ],
161
- 20: [
162
- { title: 'Danger Event', type: 'danger' }
163
- ],
152
+ 5: [{ title: "Primary Event", type: "primary" }],
153
+ 10: [{ title: "Info Event", type: "info" }],
154
+ 15: [{ title: "Warning Event", type: "warning" }],
155
+ 20: [{ title: "Danger Event", type: "danger" }],
164
156
  25: [
165
- { title: 'Primary Event', type: 'primary' },
166
- { title: 'Info Event', type: 'info' },
167
- { title: 'Warning Event', type: 'warning' },
168
- { title: 'Danger Event', type: 'danger' }
169
- ]
157
+ { title: "Primary Event", type: "primary" },
158
+ { title: "Info Event", type: "info" },
159
+ { title: "Warning Event", type: "warning" },
160
+ { title: "Danger Event", type: "danger" },
161
+ ],
170
162
  });
171
163
  });
172
164
  }
173
165
  }, 0);
174
-
166
+
175
167
  return html`
176
168
  <div class="stack-lg">
177
169
  <pds-calendar id="event-types-calendar"></pds-calendar>
178
-
170
+
179
171
  <div class="card">
180
172
  <h3>Event Types</h3>
181
173
  <ul>
182
- <li><strong>Primary:</strong> Blue - Main events and important tasks</li>
183
- <li><strong>Info:</strong> Light blue - Informational items and notes</li>
174
+ <li>
175
+ <strong>Primary:</strong> Blue - Main events and important tasks
176
+ </li>
177
+ <li>
178
+ <strong>Info:</strong> Light blue - Informational items and notes
179
+ </li>
184
180
  <li><strong>Warning:</strong> Yellow - Attention required items</li>
185
- <li><strong>Danger:</strong> Red - Critical deadlines and alerts</li>
181
+ <li>
182
+ <strong>Danger:</strong> Red - Critical deadlines and alerts
183
+ </li>
186
184
  </ul>
187
185
  </div>
188
186
  </div>
189
187
  `;
190
- }
188
+ },
191
189
  };
192
190
 
193
191
  export const DynamicEvents = {
194
192
  render: () => {
195
- setTimeout(() => {
196
- const calendar = document.querySelector('#dynamic-calendar');
197
- const addButton = document.querySelector('#add-event-btn');
198
- const dayInput = document.querySelector('#event-day-input');
199
- const titleInput = document.querySelector('#event-title-input');
200
- const typeSelect = document.querySelector('#event-type-select');
201
-
193
+ setTimeout(async () => {
194
+ if (typeof customElements?.whenDefined === "function") {
195
+ await customElements.whenDefined("pds-calendar");
196
+ }
197
+
198
+ const resolveCalendar = async () => {
199
+ for (let attempt = 0; attempt < 20; attempt += 1) {
200
+ const candidate = document.querySelector("#dynamic-calendar");
201
+ if (candidate?.shadowRoot) return candidate;
202
+ await new Promise((resolve) => requestAnimationFrame(resolve));
203
+ }
204
+ return document.querySelector("#dynamic-calendar");
205
+ };
206
+
207
+ const calendar = await resolveCalendar();
208
+ if (!calendar || calendar.__dynamicEventSetup) return;
209
+ calendar.__dynamicEventSetup = true;
210
+
202
211
  let currentEvents = {
203
- 10: [{ title: 'Existing Event', type: 'primary' }]
212
+ 10: [
213
+ {
214
+ title: "Existing Event",
215
+ type: "primary",
216
+ time: "09:00",
217
+ duration: 30,
218
+ },
219
+ ],
204
220
  };
205
-
206
- // Setup persistent event listener
207
- if (calendar) {
208
- calendar.addEventListener('month-rendered', (e) => {
209
- e.detail.fill(currentEvents);
221
+
222
+ const eventTypes = ["primary", "info", "warning", "danger"];
223
+
224
+ const normalizeEvent = (event = {}) => {
225
+ const rawTitle = String(event.title || "").trim();
226
+ const rawTime = String(event.time || "").trim();
227
+ const parsedDuration = Number(event.duration);
228
+ const normalizedDuration =
229
+ Number.isFinite(parsedDuration) && parsedDuration > 0
230
+ ? Math.round(parsedDuration)
231
+ : null;
232
+ const rawType = String(event.type || "info").trim().toLowerCase();
233
+ const normalizedType = eventTypes.includes(rawType) ? rawType : "info";
234
+
235
+ return {
236
+ title: rawTitle,
237
+ time: rawTime,
238
+ duration: normalizedDuration,
239
+ type: normalizedType,
240
+ };
241
+ };
242
+
243
+ const toCalendarEvent = (event = {}) => {
244
+ const normalized = normalizeEvent(event);
245
+ const hasDuration =
246
+ Number.isFinite(normalized.duration) && normalized.duration > 0;
247
+ const titlePrefix = normalized.time
248
+ ? hasDuration
249
+ ? `${normalized.time} (${normalized.duration}m) · `
250
+ : `${normalized.time} · `
251
+ : "";
252
+
253
+ return {
254
+ title: `${titlePrefix}${normalized.title || "Untitled"}`,
255
+ type: normalized.type,
256
+ };
257
+ };
258
+
259
+ const getDayEvents = (day) => {
260
+ if (!currentEvents[day]) {
261
+ currentEvents[day] = [];
262
+ }
263
+ return currentEvents[day];
264
+ };
265
+
266
+ const getTaskContextFromEvent = (event) => {
267
+ const task = event.target?.closest?.(".task");
268
+ if (!task) return null;
269
+
270
+ const dayCell = task.closest(".day[data-day]");
271
+ const day = Number.parseInt(dayCell?.dataset?.day || "", 10);
272
+ if (!Number.isInteger(day) || day < 1) return null;
273
+
274
+ const taskItems = [...dayCell.querySelectorAll(".task")];
275
+ const eventIndex = taskItems.indexOf(task);
276
+ if (eventIndex < 0) return null;
277
+
278
+ return { day, eventIndex };
279
+ };
280
+
281
+ const getDayFromEvent = (event) => {
282
+ const path = event.composedPath?.() || [];
283
+ for (const node of path) {
284
+ const dayValue = node?.dataset?.day;
285
+ const dayNumber = Number.parseInt(dayValue || "", 10);
286
+ if (Number.isInteger(dayNumber) && dayNumber > 0) return dayNumber;
287
+ }
288
+ return null;
289
+ };
290
+
291
+ const openEventDialog = async ({ day, eventIndex = null }) => {
292
+ const existingEvents = getDayEvents(day);
293
+ const hasExistingEvent =
294
+ Number.isInteger(eventIndex)
295
+ && eventIndex >= 0
296
+ && eventIndex < existingEvents.length;
297
+ const existingEvent = hasExistingEvent
298
+ ? normalizeEvent(existingEvents[eventIndex])
299
+ : null;
300
+
301
+ const selectedDate = new Date(calendar.year, calendar.month, day);
302
+ const selectedDateLabel = selectedDate.toLocaleDateString(undefined, {
303
+ weekday: "short",
304
+ month: "short",
305
+ day: "numeric",
306
+ year: "numeric",
210
307
  });
211
- }
212
-
213
- if (addButton && calendar) {
214
- addButton.onclick = () => {
215
- const day = parseInt(dayInput.value);
216
- const title = titleInput.value;
217
- const type = typeSelect.value;
218
-
219
- if (day && title) {
220
- if (!currentEvents[day]) {
221
- currentEvents[day] = [];
222
- }
223
- currentEvents[day].push({ title, type });
224
-
225
- // Trigger re-render
226
- calendar.refresh();
227
-
228
- // Clear inputs
229
- titleInput.value = '';
230
- dayInput.value = '';
308
+
309
+ const buttons = hasExistingEvent
310
+ ? {
311
+ delete: { name: "Delete event" },
312
+ ok: {
313
+ name: "Save changes",
314
+ primary: true,
315
+ },
316
+ cancel: { name: "Cancel", cancel: true },
231
317
  }
318
+ : {
319
+ ok: {
320
+ name: "Add event",
321
+ primary: true,
322
+ },
323
+ cancel: { name: "Cancel", cancel: true },
324
+ };
325
+
326
+ const result = await PDS.ask(
327
+ html`
328
+ <form method="dialog" class="stack-md min-w-sm">
329
+ <pds-form id="calendar-event-form" data-required hide-actions></pds-form>
330
+ <input type="hidden" name="eventPayload" value="" />
331
+ <input type="hidden" name="eventAction" value="upsert" />
332
+ </form>
333
+ `,
334
+ {
335
+ title: hasExistingEvent
336
+ ? `Edit event for ${selectedDateLabel}`
337
+ : `Add event for ${selectedDateLabel}`,
338
+ useForm: true,
339
+ buttons,
340
+ rendered(dialog) {
341
+ const hostForm = dialog.querySelector("form");
342
+ const pdsForm = dialog.querySelector("#calendar-event-form");
343
+ const hidden = dialog.querySelector('input[name="eventPayload"]');
344
+ const actionField = dialog.querySelector('input[name="eventAction"]');
345
+ const deleteButton = dialog.querySelector('button[value="delete"]');
346
+
347
+ if (!pdsForm || !hidden) return;
348
+
349
+ pdsForm.jsonSchema = {
350
+ type: "object",
351
+ required: ["title"],
352
+ properties: {
353
+ title: {
354
+ type: "string",
355
+ title: "Title",
356
+ minLength: 1,
357
+ examples: ["Sprint planning"],
358
+ },
359
+ time: {
360
+ type: "string",
361
+ format: "time",
362
+ title: "Time",
363
+ examples: ["09:30"],
364
+ },
365
+ duration: {
366
+ type: "number",
367
+ title: "Duration (minutes)",
368
+ minimum: 1,
369
+ examples: ["30"],
370
+ },
371
+ type: {
372
+ type: "string",
373
+ title: "Type",
374
+ oneOf: [
375
+ { const: "primary", title: "Primary" },
376
+ { const: "info", title: "Info" },
377
+ { const: "warning", title: "Warning" },
378
+ { const: "danger", title: "Danger" },
379
+ ],
380
+ },
381
+ },
382
+ };
383
+
384
+ pdsForm.uiSchema = {
385
+ "/title": { "ui:icon": "calendar" },
386
+ "/time": { "ui:autocomplete": "off" },
387
+ "/duration": { "ui:icon": "timer" },
388
+ "/type": { "ui:widget": "radio", "ui:class": "buttons" },
389
+ };
390
+
391
+ pdsForm.values = {
392
+ title: existingEvent?.title || "",
393
+ time: existingEvent?.time || "",
394
+ duration: existingEvent?.duration ?? null,
395
+ type: existingEvent?.type || "info",
396
+ };
397
+
398
+ const syncHidden = (detail) => {
399
+ let payload = detail?.json;
400
+ if (!payload && typeof pdsForm.serialize === "function") {
401
+ payload = pdsForm.serialize()?.json;
402
+ }
403
+ hidden.value = JSON.stringify(payload || {});
404
+ };
405
+
406
+ if (deleteButton && hostForm) {
407
+ deleteButton.className = "btn-danger btn-outline btn-sm";
408
+ deleteButton.addEventListener("click", (event) => {
409
+ event.preventDefault();
410
+ if (actionField) {
411
+ actionField.value = "delete";
412
+ }
413
+ const submitter =
414
+ hostForm.querySelector('button[value="ok"]') || undefined;
415
+ hostForm.requestSubmit(submitter);
416
+ });
417
+ }
418
+
419
+ pdsForm.addEventListener("pw:value-change", (event) => {
420
+ syncHidden(event.detail);
421
+ });
422
+
423
+ pdsForm.addEventListener("pw:submit", (event) => {
424
+ event.preventDefault();
425
+ if (actionField) {
426
+ actionField.value = "upsert";
427
+ }
428
+ syncHidden(event.detail);
429
+ const submitter =
430
+ hostForm?.querySelector('button[value="ok"]') || undefined;
431
+ hostForm?.requestSubmit(submitter);
432
+ });
433
+
434
+ hostForm?.addEventListener(
435
+ "submit",
436
+ (event) => {
437
+ if (event.submitter?.value === "ok") {
438
+ syncHidden();
439
+ }
440
+ },
441
+ true
442
+ );
443
+
444
+ customElements.whenDefined("pds-form").then(() => {
445
+ if (typeof pdsForm.serialize === "function") {
446
+ const initial = pdsForm.serialize()?.json || {};
447
+ hidden.value = JSON.stringify(initial);
448
+ }
449
+ });
450
+ },
451
+ }
452
+ );
453
+
454
+ if (!(result instanceof FormData)) return;
455
+
456
+ const action = String(result.get("eventAction") || "upsert");
457
+
458
+ if (action === "delete" && hasExistingEvent) {
459
+ const eventsForDay = getDayEvents(day);
460
+ eventsForDay.splice(eventIndex, 1);
461
+ if (!eventsForDay.length) {
462
+ delete currentEvents[day];
463
+ }
464
+ calendar.refresh();
465
+ await PDS.toast(`Deleted event on ${selectedDateLabel}.`, {
466
+ type: "warning",
467
+ });
468
+ return;
469
+ }
470
+
471
+ const raw = result.get("eventPayload");
472
+ let payload = null;
473
+ try {
474
+ payload = raw ? JSON.parse(String(raw)) : null;
475
+ } catch {
476
+ payload = null;
477
+ }
478
+
479
+ const normalized = normalizeEvent(payload || {});
480
+
481
+ if (hasExistingEvent) {
482
+ currentEvents[day][eventIndex] = normalized;
483
+ } else {
484
+ getDayEvents(day).push(normalized);
485
+ }
486
+
487
+ calendar.refresh();
488
+ await PDS.toast(
489
+ hasExistingEvent
490
+ ? `Updated event on ${selectedDateLabel}.`
491
+ : `Added event on ${selectedDateLabel}.`,
492
+ {
493
+ type: "success",
494
+ }
495
+ );
496
+ };
497
+
498
+ calendar.addEventListener("month-rendered", (e) => {
499
+ const filledEvents = Object.fromEntries(
500
+ Object.entries(currentEvents).map(([day, events]) => [
501
+ day,
502
+ events.map((item) => toCalendarEvent(item)),
503
+ ])
504
+ );
505
+ e.detail.fill(filledEvents);
506
+ });
507
+
508
+ const bindInteractions = () => {
509
+ if (!calendar.isConnected) return;
510
+
511
+ const shadow = calendar.shadowRoot;
512
+ if (!shadow) {
513
+ requestAnimationFrame(bindInteractions);
514
+ return;
515
+ }
516
+
517
+ if (shadow.__dynamicEventInteractionsSetup) return;
518
+ shadow.__dynamicEventInteractionsSetup = true;
519
+
520
+ shadow.addEventListener("dblclick", (event) => {
521
+ const taskContext = getTaskContextFromEvent(event);
522
+ if (taskContext) {
523
+ openEventDialog(taskContext);
524
+ return;
525
+ }
526
+
527
+ const day = getDayFromEvent(event);
528
+ if (!day) return;
529
+ openEventDialog({ day });
530
+ });
531
+
532
+ let longPressTimer = null;
533
+ const clearLongPress = () => {
534
+ clearTimeout(longPressTimer);
535
+ longPressTimer = null;
232
536
  };
233
- }
537
+
538
+ shadow.addEventListener("pointerdown", (event) => {
539
+ const taskContext = getTaskContextFromEvent(event);
540
+ const day = taskContext?.day || getDayFromEvent(event);
541
+ if (!day) return;
542
+ clearLongPress();
543
+ longPressTimer = setTimeout(() => {
544
+ if (taskContext) {
545
+ openEventDialog(taskContext);
546
+ } else {
547
+ openEventDialog({ day });
548
+ }
549
+ clearLongPress();
550
+ }, 550);
551
+ });
552
+
553
+ shadow.addEventListener("pointerup", clearLongPress);
554
+ shadow.addEventListener("pointercancel", clearLongPress);
555
+ shadow.addEventListener("pointerleave", clearLongPress);
556
+ };
557
+
558
+ bindInteractions();
234
559
  }, 0);
235
-
560
+
236
561
  return html`
237
562
  <div class="stack-lg">
238
563
  <div class="card">
239
564
  <h3>Add Events Dynamically</h3>
240
- <div class="flex gap-md items-end">
241
- <div class="form-group">
242
- <label for="event-day-input">Day</label>
243
- <input id="event-day-input" type="number" min="1" max="31" placeholder="Day" class="input" />
244
- </div>
245
- <div class="form-group flex-1">
246
- <label for="event-title-input">Title</label>
247
- <input id="event-title-input" type="text" placeholder="Event title" class="input" />
248
- </div>
249
- <div class="form-group">
250
- <label for="event-type-select">Type</label>
251
- <select id="event-type-select" class="input">
252
- <option value="primary">Primary</option>
253
- <option value="info">Info</option>
254
- <option value="warning">Warning</option>
255
- <option value="danger">Danger</option>
256
- </select>
257
- </div>
258
- <button id="add-event-btn" class="btn-primary">Add Event</button>
259
- </div>
565
+ <p class="text-muted">
566
+ Double-click a day (or long-press on touch) to add an event.
567
+ Double-click an existing event to edit or delete it.
568
+ </p>
260
569
  </div>
261
-
570
+
262
571
  <pds-calendar id="dynamic-calendar"></pds-calendar>
263
572
  </div>
264
573
  `;
265
- }
574
+ },
266
575
  };
267
576
 
577
+
268
578
  export const FormParticipation = {
269
579
  render: () => {
270
580
  setTimeout(() => {
271
- const form = document.querySelector('#calendar-form-example');
272
- const calendar = document.querySelector('#form-calendar');
273
- const output = document.querySelector('#calendar-form-output');
581
+ const form = document.querySelector("#calendar-form-example");
582
+ const calendar = document.querySelector("#form-calendar");
583
+ const output = document.querySelector("#calendar-form-output");
274
584
 
275
585
  if (!form || !calendar || !output) return;
276
586
 
@@ -278,22 +588,22 @@ export const FormParticipation = {
278
588
  output.textContent = value;
279
589
  };
280
590
 
281
- calendar.addEventListener('month-rendered', (event) => {
591
+ calendar.addEventListener("month-rendered", (event) => {
282
592
  event.detail.fill({
283
- 4: [{ title: 'Planning', type: 'primary' }],
284
- 9: [{ title: 'Architecture', type: 'info' }],
285
- 16: [{ title: 'Review', type: 'warning' }],
286
- 24: [{ title: 'Deadline', type: 'danger' }],
593
+ 4: [{ title: "Planning", type: "primary" }],
594
+ 9: [{ title: "Architecture", type: "info" }],
595
+ 16: [{ title: "Review", type: "warning" }],
596
+ 24: [{ title: "Deadline", type: "danger" }],
287
597
  });
288
598
  });
289
599
 
290
- calendar.value = '';
600
+ calendar.value = "";
291
601
 
292
- form.addEventListener('submit', (event) => {
602
+ form.addEventListener("submit", (event) => {
293
603
  event.preventDefault();
294
604
 
295
605
  if (!form.reportValidity()) {
296
- writeOutput('Form invalid: pds-calendar requires a date.');
606
+ writeOutput("Form invalid: pds-calendar requires a date.");
297
607
  return;
298
608
  }
299
609
 
@@ -302,10 +612,10 @@ export const FormParticipation = {
302
612
  writeOutput(JSON.stringify(payload, null, 2));
303
613
  });
304
614
 
305
- form.addEventListener('reset', () => {
615
+ form.addEventListener("reset", () => {
306
616
  setTimeout(() => {
307
- calendar.value = '';
308
- writeOutput('Form reset complete.');
617
+ calendar.value = "";
618
+ writeOutput("Form reset complete.");
309
619
  }, 0);
310
620
  });
311
621
  }, 0);
@@ -314,11 +624,12 @@ export const FormParticipation = {
314
624
  <section class="stack-lg card">
315
625
  <h3>Form Participation</h3>
316
626
  <p class="text-muted">
317
- Pick a meeting date directly in <code>&lt;pds-calendar&gt;</code>. This compact mode shows only day numbers;
318
- event days are color-coded and no event text is rendered.
627
+ Pick a meeting date directly in <code>&lt;pds-calendar&gt;</code>.
628
+ This compact mode shows only day numbers; event days are color-coded
629
+ and no event text is rendered.
319
630
  </p>
320
631
 
321
- <form id="calendar-form-example" class="stack-md">
632
+ <form id="calendar-form-example">
322
633
  <pds-calendar
323
634
  id="form-calendar"
324
635
  compact
@@ -333,8 +644,179 @@ export const FormParticipation = {
333
644
  </div>
334
645
  </form>
335
646
 
336
- <pre id="calendar-form-output" class="card surface-subtle">Submit the form to inspect payload.</pre>
647
+ <pre id="calendar-form-output" class="card surface-subtle">
648
+ Submit the form to inspect payload.</pre
649
+ >
650
+ </section>
651
+ `;
652
+ },
653
+ };
654
+
655
+ export const EventDetailsPanel = {
656
+ render: () => {
657
+ setTimeout(() => {
658
+ const calendar = document.querySelector("#details-panel-calendar");
659
+ const detailsHost = document.querySelector("#event-details-panel");
660
+ if (!calendar || !detailsHost) return;
661
+
662
+ const eventsByDay = {
663
+ 3: [
664
+ {
665
+ title: "Sprint Planning",
666
+ type: "primary",
667
+ time: "09:00",
668
+ description: "Plan sprint scope and assign owners.",
669
+ },
670
+ {
671
+ title: "Design Sync",
672
+ type: "info",
673
+ time: "14:00",
674
+ description: "Review UI updates and interaction details.",
675
+ },
676
+ ],
677
+ 8: [
678
+ {
679
+ title: "Dependency Audit",
680
+ type: "warning",
681
+ time: "11:00",
682
+ description: "Audit package updates and pending migrations.",
683
+ },
684
+ ],
685
+ 12: [
686
+ {
687
+ title: "Release Gate",
688
+ type: "danger",
689
+ time: "16:30",
690
+ description: "Final go/no-go check for release readiness.",
691
+ },
692
+ ],
693
+ 19: [
694
+ {
695
+ title: "Customer Feedback Review",
696
+ type: "info",
697
+ time: "10:30",
698
+ description: "Prioritize recent feature requests and defects.",
699
+ },
700
+ ],
701
+ };
702
+
703
+ const renderDetails = (entry, day) => {
704
+ if (!entry) {
705
+ detailsHost.innerHTML = "";
706
+ return;
707
+ }
708
+
709
+ const selectedDate = new Date(calendar.year, calendar.month, day);
710
+ const selectedDateLabel = selectedDate.toLocaleDateString(undefined, {
711
+ weekday: "short",
712
+ month: "short",
713
+ day: "numeric",
714
+ year: "numeric",
715
+ });
716
+
717
+ detailsHost.innerHTML = `
718
+ <div class="stack-sm">
719
+ <p>${selectedDateLabel}</p>
720
+ <h3>${entry.title}</h3>
721
+ <p class="text-muted">${entry.time}</p>
722
+ <p>${entry.description}</p>
723
+ </div>
724
+ `;
725
+ };
726
+
727
+ const renderDayDetails = (day) => {
728
+ const selectedDate = new Date(calendar.year, calendar.month, day);
729
+ const selectedDateLabel = selectedDate.toLocaleDateString(undefined, {
730
+ weekday: "short",
731
+ month: "short",
732
+ day: "numeric",
733
+ year: "numeric",
734
+ });
735
+
736
+ const list = eventsByDay[day] || [];
737
+ if (!list.length) {
738
+ detailsHost.innerHTML = `
739
+ <section class="flex flex-col items-center">
740
+
741
+ <h5>No events on ${selectedDateLabel}.</h5>
742
+ <small class="text-muted">Select another day in the celndar.
743
+ </small>
744
+
745
+ </section>`;
746
+ return;
747
+ }
748
+
749
+ detailsHost.innerHTML = `
750
+ <div class="stack-sm">
751
+ <p>${selectedDateLabel}</p>
752
+ ${list
753
+ .map(
754
+ (entry) => `
755
+ <article>
756
+ <header>
757
+ <h4>${entry.title}</h4>
758
+ <small class="text-muted">${entry.time}</small>
759
+ </header>
760
+ <p>${entry.description}</p>
761
+ </article>
762
+ `,
763
+ )
764
+ .join("")}
765
+ </div>
766
+ `;
767
+ };
768
+
769
+ calendar.addEventListener("month-rendered", (event) => {
770
+ event.detail.fill(eventsByDay);
771
+ });
772
+
773
+ if (!calendar.dataset.detailsClickBound) {
774
+ calendar.dataset.detailsClickBound = "true";
775
+
776
+ calendar.shadowRoot?.addEventListener("change", (event) => {
777
+ const radio = event.target?.closest?.(".day-radio-input[data-day]");
778
+ if (!radio) return;
779
+
780
+ const day = Number.parseInt(radio.dataset.day || "", 10);
781
+ if (!Number.isInteger(day)) return;
782
+
783
+ renderDayDetails(day);
784
+ });
785
+
786
+ calendar.shadowRoot?.addEventListener("click", (event) => {
787
+ const task = event.target?.closest?.(".task");
788
+ if (!task) return;
789
+
790
+ const dayCell = task.closest(".day[data-day]");
791
+ const day = Number.parseInt(dayCell?.dataset?.day || "", 10);
792
+ if (!Number.isInteger(day)) return;
793
+
794
+ const taskItems = [...dayCell.querySelectorAll(".task")];
795
+ const taskIndex = taskItems.indexOf(task);
796
+ if (taskIndex < 0) return;
797
+
798
+ const details = eventsByDay[day]?.[taskIndex] || null;
799
+ renderDetails(details, day);
800
+ });
801
+ }
802
+
803
+ renderDetails(null);
804
+ }, 0);
805
+
806
+ return html`
807
+ <section>
808
+ <header>
809
+ <h3>Event Calendar</h3>
810
+ <small class="text-muted"
811
+ >Click calendar to show event details.</small
812
+ >
813
+ </header>
814
+ <article class="flex gap-lg flex-wrap card max-w-md">
815
+ <pds-calendar id="details-panel-calendar" compact></pds-calendar>
816
+ <article id="event-details-panel"></article>
817
+ </article>
818
+
337
819
  </section>
338
820
  `;
339
- }
821
+ },
340
822
  };