phoenix_duskmoon 9.8.0 → 9.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,7 +141,7 @@ Components that require hooks:
141
141
  ## Available Components
142
142
 
143
143
  - **Action**: buttons, dropdowns, links, menus, toggles
144
- - **Data Display**: accordion, avatar, badge, card, chip, collapse, flash, git repository, list, markdown, pagination, popover, progress, skeleton, stat, table, timeline, tooltip
144
+ - **Data Display**: accordion, avatar, badge, card, chip, collapse, flash, git repository, list, markdown, markdown body, pagination, popover, progress, skeleton, stat, table, timeline, tooltip
145
145
  - **Data Entry**: autocomplete, cascader, checkbox, compact input, file upload, form, input, multi-select, OTP input, PIN input, radio, rating, segment control, select, slider, switch, textarea, time input, tree select
146
146
  - **Feedback**: dialog, loading, snackbar, toast
147
147
  - **Navigation**: actionbar, appbar, bottom nav, breadcrumb, left menu, navbar, nested menu, page footer, page header, stepper, steps, tabs
@@ -11,6 +11,11 @@ const DM_EVENTS = [
11
11
  "dm-submit", "dm-select", "dm-close", "dm-open", "dm-toggle",
12
12
  ];
13
13
 
14
+ const COPY_SELECTOR = "[data-copy-value]";
15
+ const COPY_FEEDBACK_DURATION = 2000;
16
+ const COPY_LISTENER = Symbol.for("phoenix-duskmoon.copy-listener");
17
+ const copyFeedback = new WeakMap();
18
+
14
19
  /**
15
20
  * WebComponentHook - Universal LiveView ↔ Custom Element bridge
16
21
  *
@@ -254,6 +259,108 @@ export const FormElementHook = {
254
259
  },
255
260
  };
256
261
 
262
+ /** Copy text with the Clipboard API, falling back only when it is unavailable. */
263
+ export async function copyTextToClipboard(value, environment = globalThis) {
264
+ const clipboard = environment.navigator?.clipboard;
265
+
266
+ if (typeof clipboard?.writeText === "function") {
267
+ await clipboard.writeText(value);
268
+ return;
269
+ }
270
+
271
+ const document = environment.document;
272
+
273
+ if (!document?.body || typeof document.execCommand !== "function") {
274
+ throw new Error("Clipboard API is unavailable");
275
+ }
276
+
277
+ const textArea = document.createElement("textarea");
278
+ const activeElement = document.activeElement;
279
+ textArea.value = value;
280
+ textArea.setAttribute("readonly", "");
281
+ textArea.style.position = "fixed";
282
+ textArea.style.opacity = "0";
283
+ document.body.appendChild(textArea);
284
+
285
+ try {
286
+ textArea.focus();
287
+ textArea.select();
288
+
289
+ if (!document.execCommand("copy")) {
290
+ throw new Error("Clipboard copy command failed");
291
+ }
292
+ } finally {
293
+ document.body.removeChild(textArea);
294
+ activeElement?.focus?.();
295
+ }
296
+ }
297
+
298
+ function copyStatus(button) {
299
+ const sibling = button.nextElementSibling;
300
+
301
+ if (sibling?.matches?.("[data-copy-status]")) {
302
+ return sibling;
303
+ }
304
+
305
+ return button.parentElement?.querySelector?.("[data-copy-status]") ?? null;
306
+ }
307
+
308
+ function showCopyFeedback(button, message, state, options) {
309
+ const label = button.querySelector?.("[data-copy-label]");
310
+ const status = copyStatus(button);
311
+ const previous = copyFeedback.get(button);
312
+ const cancelReset = options.cancelReset ?? globalThis.clearTimeout;
313
+ const scheduleReset = options.scheduleReset ?? globalThis.setTimeout;
314
+
315
+ if (previous?.timer !== undefined) {
316
+ cancelReset(previous.timer);
317
+ }
318
+
319
+ const originalLabel = previous?.originalLabel ?? label?.textContent ?? "Copy";
320
+
321
+ if (label) label.textContent = message;
322
+ if (status) status.textContent = message;
323
+ button.dataset.copyState = state;
324
+
325
+ const timer = scheduleReset(() => {
326
+ if (label) label.textContent = originalLabel;
327
+ if (status) status.textContent = "";
328
+ delete button.dataset.copyState;
329
+ copyFeedback.delete(button);
330
+ }, COPY_FEEDBACK_DURATION);
331
+
332
+ copyFeedback.set(button, { originalLabel, timer });
333
+ }
334
+
335
+ /** Handle clicks from any descendant of a generated copy control. */
336
+ export async function handleClipboardClick(event, options = {}) {
337
+ const button = event.target?.closest?.(COPY_SELECTOR);
338
+
339
+ if (!button || button.disabled || button.getAttribute?.("aria-disabled") === "true") {
340
+ return;
341
+ }
342
+
343
+ const copy = options.copy ?? copyTextToClipboard;
344
+
345
+ try {
346
+ await copy(button.dataset.copyValue);
347
+ showCopyFeedback(button, button.dataset.copySuccess ?? "Copied", "success", options);
348
+ } catch (_error) {
349
+ showCopyFeedback(button, button.dataset.copyFailure ?? "Copy failed", "error", options);
350
+ }
351
+ }
352
+
353
+ /** Install one delegated listener so LiveView-patched copy controls also work. */
354
+ export function installClipboardBehavior(root = globalThis.document, options = {}) {
355
+ if (!root?.addEventListener || root[COPY_LISTENER]) return;
356
+
357
+ const listener = (event) => handleClipboardClick(event, options);
358
+ root.addEventListener("click", listener);
359
+ root[COPY_LISTENER] = listener;
360
+ }
361
+
362
+ installClipboardBehavior();
363
+
257
364
  // Export to window for non-module usage
258
365
  if (typeof window !== "undefined") {
259
366
  window.__WebComponentHook__ = WebComponentHook;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_duskmoon",
3
- "version": "9.8.0",
3
+ "version": "9.9.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./priv/static/assets/js/phoenix_duskmoon.js",
@@ -35,17 +35,17 @@
35
35
  "registry": "https://registry.npmjs.org/"
36
36
  },
37
37
  "dependencies": {
38
- "@duskmoon-dev/art-elements": "1.5.5",
38
+ "@duskmoon-dev/art-elements": "1.5.6",
39
39
  "@duskmoon-dev/core": "1.17.0",
40
40
  "@duskmoon-dev/css-art": "1.17.0",
41
- "@duskmoon-dev/elements": "1.5.5"
41
+ "@duskmoon-dev/elements": "1.5.6"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@mdi/svg": "7.4.47",
45
45
  "bootstrap-icons": "1.13.1",
46
- "postcss": "8.5.16"
46
+ "postcss": "8.5.19"
47
47
  },
48
48
  "peerDependencies": {
49
- "tailwindcss": "4.3.2"
49
+ "tailwindcss": "4.3.3"
50
50
  }
51
51
  }