@dumiflex/wp-kit 0.3.0 → 0.5.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 (130) hide show
  1. package/CHANGELOG.md +92 -51
  2. package/LICENSE +674 -674
  3. package/README.md +73 -76
  4. package/package.json +78 -77
  5. package/src/Avatar.vue +83 -83
  6. package/src/Badge.vue +97 -97
  7. package/src/Banner.vue +118 -118
  8. package/src/BookmarkButton.vue +27 -27
  9. package/src/BundleMeta.vue +51 -51
  10. package/src/Button.vue +103 -103
  11. package/src/Card.vue +68 -68
  12. package/src/Checkbox.vue +109 -107
  13. package/src/Chip.vue +163 -97
  14. package/src/ChoiceCards.vue +133 -133
  15. package/src/CloseButton.vue +69 -69
  16. package/src/CodeBlock.vue +146 -119
  17. package/src/ColorField.vue +188 -206
  18. package/src/ColorPicker.vue +201 -201
  19. package/src/Combobox.vue +331 -334
  20. package/src/ConfirmDialog.vue +165 -163
  21. package/src/ConfirmModal.vue +147 -147
  22. package/src/ConstraintMatrixPreview.vue +708 -708
  23. package/src/ContextMenu.vue +386 -379
  24. package/src/CopyButton.vue +156 -156
  25. package/src/DataTable.vue +293 -292
  26. package/src/DatePicker.vue +234 -234
  27. package/src/DescriptionList.vue +73 -73
  28. package/src/DiffView.vue +68 -68
  29. package/src/Disclosure.vue +166 -166
  30. package/src/DragHandle.vue +49 -49
  31. package/src/Drawer.vue +131 -131
  32. package/src/DropZone.vue +196 -196
  33. package/src/DropdownMenu.vue +72 -72
  34. package/src/EmptyState.vue +111 -111
  35. package/src/Field.vue +28 -28
  36. package/src/HsvPicker.vue +208 -208
  37. package/src/Icon.vue +39 -39
  38. package/src/IconPicker.vue +209 -209
  39. package/src/ImageGrid.vue +127 -125
  40. package/src/InlineEdit.vue +183 -183
  41. package/src/Input.vue +99 -99
  42. package/src/Kbd.vue +56 -56
  43. package/src/Lightbox.vue +176 -173
  44. package/src/ListFilter.vue +85 -85
  45. package/src/ListRow.vue +204 -204
  46. package/src/LoadingMark.vue +217 -161
  47. package/src/Logo.vue +60 -60
  48. package/src/Modal.vue +78 -76
  49. package/src/ModalShell.vue +109 -107
  50. package/src/Pagination.vue +106 -106
  51. package/src/Popover.vue +180 -171
  52. package/src/PostCard.vue +210 -211
  53. package/src/ProgressBar.vue +90 -90
  54. package/src/RadioGroup.vue +177 -177
  55. package/src/RangeSlider.vue +183 -183
  56. package/src/RelativeDate.vue +35 -35
  57. package/src/ResizeHandle.vue +143 -143
  58. package/src/SearchInput.vue +192 -199
  59. package/src/Select.vue +644 -641
  60. package/src/Separator.vue +54 -54
  61. package/src/Skeleton.vue +98 -82
  62. package/src/Slider.vue +198 -198
  63. package/src/SortableList.vue +260 -260
  64. package/src/SplitPane.vue +68 -68
  65. package/src/StarButton.vue +40 -40
  66. package/src/Stepper.vue +141 -141
  67. package/src/SupportLinks.vue +93 -93
  68. package/src/TagInput.vue +297 -300
  69. package/src/Textarea.vue +57 -57
  70. package/src/Timeline.vue +92 -92
  71. package/src/ToastCard.vue +119 -119
  72. package/src/ToastHost.vue +40 -40
  73. package/src/Toggle.vue +36 -36
  74. package/src/TokenizedValue.vue +310 -310
  75. package/src/Toolbar.vue +110 -110
  76. package/src/VirtualList.vue +87 -87
  77. package/src/VisuallyHidden.vue +29 -29
  78. package/src/WpButton.vue +75 -75
  79. package/src/WpCheck.vue +104 -102
  80. package/src/WpIconButton.vue +107 -107
  81. package/src/WpImageZoom.vue +544 -544
  82. package/src/WpNumber.vue +178 -183
  83. package/src/WpSegmented.vue +175 -175
  84. package/src/WpSelect.vue +93 -89
  85. package/src/WpSwitch.vue +163 -163
  86. package/src/WpTabs.vue +188 -188
  87. package/src/_modal-motion.css +33 -33
  88. package/src/_skeleton.css +50 -50
  89. package/src/axis-color.ts +42 -42
  90. package/src/button.css +169 -169
  91. package/src/calendar.ts +59 -59
  92. package/src/clipboard.ts +50 -50
  93. package/src/colour-presets.ts +12 -12
  94. package/src/diff.ts +51 -51
  95. package/src/drop-zone.ts +41 -41
  96. package/src/field.css +50 -0
  97. package/src/flip.ts +237 -237
  98. package/src/floating-layer.ts +48 -0
  99. package/src/floating.ts +99 -99
  100. package/src/highlight.ts +199 -0
  101. package/src/hsv.ts +100 -100
  102. package/src/icon-catalog.ts +52 -52
  103. package/src/image-zoom.ts +93 -93
  104. package/src/kind-colour.ts +28 -0
  105. package/src/loading-mark-motion.ts +249 -0
  106. package/src/logo-paths.ts +21 -21
  107. package/src/matrix-axis.ts +91 -91
  108. package/src/pagination.ts +27 -27
  109. package/src/platform-keys.ts +29 -29
  110. package/src/rich-tokenize.ts +357 -357
  111. package/src/select-types.ts +32 -32
  112. package/src/slider-math.ts +45 -45
  113. package/src/sortable.ts +22 -22
  114. package/src/theme/fonts/OFL-Fraunces.txt +93 -93
  115. package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
  116. package/src/theme/fonts/OFL-Manrope.txt +93 -93
  117. package/src/theme/looks.css +428 -428
  118. package/src/theme/palettes.css +254 -254
  119. package/src/theme/tokens.css +2896 -2894
  120. package/src/tokens.ts +56 -56
  121. package/src/tooltip.css +43 -43
  122. package/src/tooltip.ts +146 -146
  123. package/src/useDialogFocus.ts +73 -73
  124. package/src/useGrowableField.ts +357 -357
  125. package/src/useModalKeyShield.ts +116 -116
  126. package/src/useToast.ts +48 -48
  127. package/src/wp-logo.svg +14 -14
  128. package/src/ChipToggle.vue +0 -92
  129. package/src/KindChip.vue +0 -53
  130. package/src/RatingBadge.vue +0 -42
package/src/Toolbar.vue CHANGED
@@ -1,110 +1,110 @@
1
- <script setup lang="ts">
2
- /**
3
- * Toolbar: a row of controls that is one Tab stop. Inside it the arrow keys
4
- * move between the controls, Home/End jump to the ends, and the toolbar
5
- * remembers where you were (APG toolbar). `attached` joins its buttons into
6
- * one button group. Works on the canvas and in the manager.
7
- *
8
- * <Toolbar label="Text">
9
- * <Button variant="ghost" icon="pi-bold" aria-label="Bold" />
10
- * <Separator orientation="vertical" />
11
- * <Button variant="ghost" icon="pi-copy" aria-label="Copy" />
12
- * </Toolbar>
13
- *
14
- * Put buttons, toggles and selects in it. A text box inside a toolbar keeps
15
- * its own left/right keys.
16
- */
17
- import { onMounted, onUpdated, ref } from "vue";
18
-
19
- const props = withDefaults(defineProps<{
20
- /** The toolbar's name for screen readers. */
21
- label: string;
22
- orientation?: "horizontal" | "vertical" | undefined;
23
- /** Join the buttons into one group with shared borders. */
24
- attached?: boolean | undefined;
25
- /** Gap between controls. */
26
- size?: "sm" | "md" | undefined;
27
- }>(), { orientation: "horizontal", size: "md" });
28
-
29
- const rootEl = ref<HTMLElement | null>(null);
30
- const last = ref(0);
31
-
32
- const ITEMS = "button:not([disabled]), [role=button]:not([aria-disabled=true]), a[href], input:not([disabled]), select:not([disabled]), [role=switch], [role=checkbox], [role=radio]";
33
-
34
- function items(): HTMLElement[] {
35
- return rootEl.value ? [...rootEl.value.querySelectorAll<HTMLElement>(ITEMS)] : [];
36
- }
37
-
38
- /** Roving tabindex: only the remembered control is in the Tab order. */
39
- function sync(): void {
40
- const list = items();
41
- if (!list.length) return;
42
- const at = Math.min(last.value, list.length - 1);
43
- list.forEach((el, i) => el.setAttribute("tabindex", i === at ? "0" : "-1"));
44
- }
45
-
46
- function isTextField(el: Element | null): boolean {
47
- return !!el && el.tagName === "INPUT" && !["checkbox", "radio", "button"].includes((el as HTMLInputElement).type);
48
- }
49
-
50
- function onKeydown(ev: KeyboardEvent): void {
51
- const list = items();
52
- const at = list.indexOf(document.activeElement as HTMLElement);
53
- if (at < 0) return;
54
- const fwd = props.orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
55
- const back = props.orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
56
- if (isTextField(document.activeElement) && (ev.key === "ArrowLeft" || ev.key === "ArrowRight" || ev.key === "Home" || ev.key === "End")) return;
57
- let next: number;
58
- if (ev.key === fwd) next = (at + 1) % list.length;
59
- else if (ev.key === back) next = (at - 1 + list.length) % list.length;
60
- else if (ev.key === "Home") next = 0;
61
- else if (ev.key === "End") next = list.length - 1;
62
- else return;
63
- ev.preventDefault();
64
- last.value = next;
65
- sync();
66
- list[next].focus();
67
- }
68
-
69
- function onFocusin(ev: FocusEvent): void {
70
- const i = items().indexOf(ev.target as HTMLElement);
71
- if (i >= 0 && i !== last.value) { last.value = i; sync(); }
72
- }
73
-
74
- onMounted(sync);
75
- onUpdated(sync);
76
- </script>
77
-
78
- <!-- role="toolbar" moves focus between its own controls. -->
79
- <!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
80
- <template>
81
- <div
82
- ref="rootEl"
83
- class="wp-tools"
84
- role="toolbar"
85
- :aria-label="label"
86
- :aria-orientation="orientation"
87
- :data-orientation="orientation"
88
- :data-attached="attached || undefined"
89
- :data-size="size"
90
- @keydown="onKeydown"
91
- @focusin="onFocusin"
92
- >
93
- <slot />
94
- </div>
95
- </template>
96
-
97
- <style scoped>
98
- .wp-tools { display: inline-flex; align-items: center; gap: var(--wp-space-2); flex-wrap: wrap; min-width: 0; }
99
- .wp-tools[data-size="md"] { gap: var(--wp-space-3); }
100
- .wp-tools[data-orientation="vertical"] { flex-direction: column; align-items: stretch; flex-wrap: nowrap; }
101
- .wp-tools[data-attached] { gap: 0; flex-wrap: nowrap; }
102
- .wp-tools[data-attached] > :deep(*) { border-radius: 0; }
103
- .wp-tools[data-attached] > :deep(*:not(:first-child)) { margin-left: -1px; }
104
- .wp-tools[data-attached] > :deep(*:first-child) { border-top-left-radius: var(--wp-radius); border-bottom-left-radius: var(--wp-radius); }
105
- .wp-tools[data-attached] > :deep(*:last-child) { border-top-right-radius: var(--wp-radius); border-bottom-right-radius: var(--wp-radius); }
106
- .wp-tools[data-attached] > :deep(*:focus-visible) { position: relative; z-index: 1; }
107
- .wp-tools[data-attached][data-orientation="vertical"] > :deep(*:not(:first-child)) { margin-left: 0; margin-top: -1px; }
108
- .wp-tools[data-attached][data-orientation="vertical"] > :deep(*:first-child) { border-radius: var(--wp-radius) var(--wp-radius) 0 0; }
109
- .wp-tools[data-attached][data-orientation="vertical"] > :deep(*:last-child) { border-radius: 0 0 var(--wp-radius) var(--wp-radius); }
110
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Toolbar: a row of controls that is one Tab stop. Inside it the arrow keys
4
+ * move between the controls, Home/End jump to the ends, and the toolbar
5
+ * remembers where you were (APG toolbar). `attached` joins its buttons into
6
+ * one button group. Works on the canvas and in the manager.
7
+ *
8
+ * <Toolbar label="Text">
9
+ * <Button variant="ghost" icon="pi-bold" aria-label="Bold" />
10
+ * <Separator orientation="vertical" />
11
+ * <Button variant="ghost" icon="pi-copy" aria-label="Copy" />
12
+ * </Toolbar>
13
+ *
14
+ * Put buttons, toggles and selects in it. A text box inside a toolbar keeps
15
+ * its own left/right keys.
16
+ */
17
+ import { onMounted, onUpdated, ref } from "vue";
18
+
19
+ const props = withDefaults(defineProps<{
20
+ /** The toolbar's name for screen readers. */
21
+ label: string;
22
+ orientation?: "horizontal" | "vertical" | undefined;
23
+ /** Join the buttons into one group with shared borders. */
24
+ attached?: boolean | undefined;
25
+ /** Gap between controls. */
26
+ size?: "sm" | "md" | undefined;
27
+ }>(), { orientation: "horizontal", size: "md" });
28
+
29
+ const rootEl = ref<HTMLElement | null>(null);
30
+ const last = ref(0);
31
+
32
+ const ITEMS = "button:not([disabled]), [role=button]:not([aria-disabled=true]), a[href], input:not([disabled]), select:not([disabled]), [role=switch], [role=checkbox], [role=radio]";
33
+
34
+ function items(): HTMLElement[] {
35
+ return rootEl.value ? [...rootEl.value.querySelectorAll<HTMLElement>(ITEMS)] : [];
36
+ }
37
+
38
+ /** Roving tabindex: only the remembered control is in the Tab order. */
39
+ function sync(): void {
40
+ const list = items();
41
+ if (!list.length) return;
42
+ const at = Math.min(last.value, list.length - 1);
43
+ list.forEach((el, i) => el.setAttribute("tabindex", i === at ? "0" : "-1"));
44
+ }
45
+
46
+ function isTextField(el: Element | null): boolean {
47
+ return !!el && el.tagName === "INPUT" && !["checkbox", "radio", "button"].includes((el as HTMLInputElement).type);
48
+ }
49
+
50
+ function onKeydown(ev: KeyboardEvent): void {
51
+ const list = items();
52
+ const at = list.indexOf(document.activeElement as HTMLElement);
53
+ if (at < 0) return;
54
+ const fwd = props.orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
55
+ const back = props.orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
56
+ if (isTextField(document.activeElement) && (ev.key === "ArrowLeft" || ev.key === "ArrowRight" || ev.key === "Home" || ev.key === "End")) return;
57
+ let next: number;
58
+ if (ev.key === fwd) next = (at + 1) % list.length;
59
+ else if (ev.key === back) next = (at - 1 + list.length) % list.length;
60
+ else if (ev.key === "Home") next = 0;
61
+ else if (ev.key === "End") next = list.length - 1;
62
+ else return;
63
+ ev.preventDefault();
64
+ last.value = next;
65
+ sync();
66
+ list[next].focus();
67
+ }
68
+
69
+ function onFocusin(ev: FocusEvent): void {
70
+ const i = items().indexOf(ev.target as HTMLElement);
71
+ if (i >= 0 && i !== last.value) { last.value = i; sync(); }
72
+ }
73
+
74
+ onMounted(sync);
75
+ onUpdated(sync);
76
+ </script>
77
+
78
+ <!-- role="toolbar" moves focus between its own controls. -->
79
+ <!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
80
+ <template>
81
+ <div
82
+ ref="rootEl"
83
+ class="wp-tools"
84
+ role="toolbar"
85
+ :aria-label="label"
86
+ :aria-orientation="orientation"
87
+ :data-orientation="orientation"
88
+ :data-attached="attached || undefined"
89
+ :data-size="size"
90
+ @keydown="onKeydown"
91
+ @focusin="onFocusin"
92
+ >
93
+ <slot />
94
+ </div>
95
+ </template>
96
+
97
+ <style scoped>
98
+ .wp-tools { display: inline-flex; align-items: center; gap: var(--wp-space-2); flex-wrap: wrap; min-width: 0; }
99
+ .wp-tools[data-size="md"] { gap: var(--wp-space-3); }
100
+ .wp-tools[data-orientation="vertical"] { flex-direction: column; align-items: stretch; flex-wrap: nowrap; }
101
+ .wp-tools[data-attached] { gap: 0; flex-wrap: nowrap; }
102
+ .wp-tools[data-attached] > :deep(*) { border-radius: 0; }
103
+ .wp-tools[data-attached] > :deep(*:not(:first-child)) { margin-left: -1px; }
104
+ .wp-tools[data-attached] > :deep(*:first-child) { border-top-left-radius: var(--wp-radius); border-bottom-left-radius: var(--wp-radius); }
105
+ .wp-tools[data-attached] > :deep(*:last-child) { border-top-right-radius: var(--wp-radius); border-bottom-right-radius: var(--wp-radius); }
106
+ .wp-tools[data-attached] > :deep(*:focus-visible) { position: relative; z-index: 1; }
107
+ .wp-tools[data-attached][data-orientation="vertical"] > :deep(*:not(:first-child)) { margin-left: 0; margin-top: -1px; }
108
+ .wp-tools[data-attached][data-orientation="vertical"] > :deep(*:first-child) { border-radius: var(--wp-radius) var(--wp-radius) 0 0; }
109
+ .wp-tools[data-attached][data-orientation="vertical"] > :deep(*:last-child) { border-radius: 0 0 var(--wp-radius) var(--wp-radius); }
110
+ </style>
@@ -1,87 +1,87 @@
1
- <script setup lang="ts" generic="T">
2
- /**
3
- * Virtual list: a long list (thousands of options, every library item)
4
- * where only the rows in view are built, so it scrolls smoothly. Rows have
5
- * one fixed height. Works on the canvas and in the manager.
6
- *
7
- * <VirtualList :items="options" :item-height="32" height="320px" :item-key="(o) => o.id" label="Options">
8
- * <template #item="{ item, index }">…</template>
9
- * </VirtualList>
10
- *
11
- * The rows keep their place in the list for screen readers
12
- * (`aria-setsize` / `aria-posinset`). `scrollToIndex(i)` brings a row into
13
- * view, e.g. the one a keyboard user moved to.
14
- */
15
- import { computed, onBeforeUnmount, onMounted, ref } from "vue";
16
-
17
- const props = withDefaults(defineProps<{
18
- items: T[];
19
- itemKey: (item: T, index: number) => string | number;
20
- /** Every row's height in px. */
21
- itemHeight: number;
22
- /** CSS height of the scrolling box. */
23
- height?: string | undefined;
24
- /** Rows built above and below the view. */
25
- overscan?: number | undefined;
26
- label?: string | undefined;
27
- }>(), { height: "320px", overscan: 6, label: undefined });
28
-
29
- const boxEl = ref<HTMLElement | null>(null);
30
- const top = ref(0);
31
- const viewH = ref(320);
32
-
33
- const first = computed(() => Math.max(0, Math.floor(top.value / props.itemHeight) - props.overscan));
34
- const last = computed(() => Math.min(props.items.length, Math.ceil((top.value + viewH.value) / props.itemHeight) + props.overscan));
35
- const shown = computed(() => props.items.slice(first.value, last.value).map((item, k) => ({ item, index: first.value + k })));
36
-
37
- function onScroll(): void {
38
- top.value = boxEl.value?.scrollTop ?? 0;
39
- }
40
-
41
- let ro: ResizeObserver | null = null;
42
- onMounted(() => {
43
- viewH.value = boxEl.value?.clientHeight || viewH.value;
44
- if (typeof ResizeObserver !== "undefined" && boxEl.value) {
45
- ro = new ResizeObserver(() => { viewH.value = boxEl.value?.clientHeight || viewH.value; });
46
- ro.observe(boxEl.value);
47
- }
48
- });
49
- onBeforeUnmount(() => ro?.disconnect());
50
-
51
- /** Scroll so row `i` is in view (nearest edge). */
52
- function scrollToIndex(i: number): void {
53
- const el = boxEl.value;
54
- if (!el) return;
55
- const y = i * props.itemHeight;
56
- if (y < el.scrollTop) el.scrollTop = y;
57
- else if (y + props.itemHeight > el.scrollTop + el.clientHeight) el.scrollTop = y + props.itemHeight - el.clientHeight;
58
- onScroll();
59
- }
60
-
61
- defineExpose({ scrollToIndex });
62
- </script>
63
-
64
- <template>
65
- <div ref="boxEl" class="wp-vlist" :style="{ height }" tabindex="0" role="list" :aria-label="label" @scroll.passive="onScroll">
66
- <div class="wp-vlist__space" :style="{ height: `${items.length * itemHeight}px` }">
67
- <div
68
- v-for="row in shown"
69
- :key="itemKey(row.item, row.index)"
70
- class="wp-vlist__row"
71
- role="listitem"
72
- :aria-setsize="items.length"
73
- :aria-posinset="row.index + 1"
74
- :style="{ top: `${row.index * itemHeight}px`, height: `${itemHeight}px` }"
75
- >
76
- <slot name="item" :item="row.item" :index="row.index" />
77
- </div>
78
- </div>
79
- </div>
80
- </template>
81
-
82
- <style scoped>
83
- .wp-vlist { position: relative; overflow-y: auto; overscroll-behavior: contain; min-width: 0; outline: none; border-radius: var(--wp-radius); }
84
- .wp-vlist:focus-visible { box-shadow: inset 0 0 0 2px var(--wp-border-focus); }
85
- .wp-vlist__space { position: relative; width: 100%; }
86
- .wp-vlist__row { position: absolute; left: 0; right: 0; display: flex; align-items: center; min-width: 0; box-sizing: border-box; }
87
- </style>
1
+ <script setup lang="ts" generic="T">
2
+ /**
3
+ * Virtual list: a long list (thousands of options, every library item)
4
+ * where only the rows in view are built, so it scrolls smoothly. Rows have
5
+ * one fixed height. Works on the canvas and in the manager.
6
+ *
7
+ * <VirtualList :items="options" :item-height="32" height="320px" :item-key="(o) => o.id" label="Options">
8
+ * <template #item="{ item, index }">…</template>
9
+ * </VirtualList>
10
+ *
11
+ * The rows keep their place in the list for screen readers
12
+ * (`aria-setsize` / `aria-posinset`). `scrollToIndex(i)` brings a row into
13
+ * view, e.g. the one a keyboard user moved to.
14
+ */
15
+ import { computed, onBeforeUnmount, onMounted, ref } from "vue";
16
+
17
+ const props = withDefaults(defineProps<{
18
+ items: T[];
19
+ itemKey: (item: T, index: number) => string | number;
20
+ /** Every row's height in px. */
21
+ itemHeight: number;
22
+ /** CSS height of the scrolling box. */
23
+ height?: string | undefined;
24
+ /** Rows built above and below the view. */
25
+ overscan?: number | undefined;
26
+ label?: string | undefined;
27
+ }>(), { height: "320px", overscan: 6, label: undefined });
28
+
29
+ const boxEl = ref<HTMLElement | null>(null);
30
+ const top = ref(0);
31
+ const viewH = ref(320);
32
+
33
+ const first = computed(() => Math.max(0, Math.floor(top.value / props.itemHeight) - props.overscan));
34
+ const last = computed(() => Math.min(props.items.length, Math.ceil((top.value + viewH.value) / props.itemHeight) + props.overscan));
35
+ const shown = computed(() => props.items.slice(first.value, last.value).map((item, k) => ({ item, index: first.value + k })));
36
+
37
+ function onScroll(): void {
38
+ top.value = boxEl.value?.scrollTop ?? 0;
39
+ }
40
+
41
+ let ro: ResizeObserver | null = null;
42
+ onMounted(() => {
43
+ viewH.value = boxEl.value?.clientHeight || viewH.value;
44
+ if (typeof ResizeObserver !== "undefined" && boxEl.value) {
45
+ ro = new ResizeObserver(() => { viewH.value = boxEl.value?.clientHeight || viewH.value; });
46
+ ro.observe(boxEl.value);
47
+ }
48
+ });
49
+ onBeforeUnmount(() => ro?.disconnect());
50
+
51
+ /** Scroll so row `i` is in view (nearest edge). */
52
+ function scrollToIndex(i: number): void {
53
+ const el = boxEl.value;
54
+ if (!el) return;
55
+ const y = i * props.itemHeight;
56
+ if (y < el.scrollTop) el.scrollTop = y;
57
+ else if (y + props.itemHeight > el.scrollTop + el.clientHeight) el.scrollTop = y + props.itemHeight - el.clientHeight;
58
+ onScroll();
59
+ }
60
+
61
+ defineExpose({ scrollToIndex });
62
+ </script>
63
+
64
+ <template>
65
+ <div ref="boxEl" class="wp-vlist" :style="{ height }" tabindex="0" role="list" :aria-label="label" @scroll.passive="onScroll">
66
+ <div class="wp-vlist__space" :style="{ height: `${items.length * itemHeight}px` }">
67
+ <div
68
+ v-for="row in shown"
69
+ :key="itemKey(row.item, row.index)"
70
+ class="wp-vlist__row"
71
+ role="listitem"
72
+ :aria-setsize="items.length"
73
+ :aria-posinset="row.index + 1"
74
+ :style="{ top: `${row.index * itemHeight}px`, height: `${itemHeight}px` }"
75
+ >
76
+ <slot name="item" :item="row.item" :index="row.index" />
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <style scoped>
83
+ .wp-vlist { position: relative; overflow-y: auto; overscroll-behavior: contain; min-width: 0; outline: none; border-radius: var(--wp-radius); }
84
+ .wp-vlist:focus-visible { box-shadow: inset 0 0 0 2px var(--wp-border-focus); }
85
+ .wp-vlist__space { position: relative; width: 100%; }
86
+ .wp-vlist__row { position: absolute; left: 0; right: 0; display: flex; align-items: center; min-width: 0; box-sizing: border-box; }
87
+ </style>
@@ -1,29 +1,29 @@
1
- <script setup lang="ts">
2
- /**
3
- * Visually hidden: text for screen readers only (a table header with no
4
- * visible name, "(opens in a new tab)", a live status line). With
5
- * `focusable` it shows itself while it has focus, for a "Skip to the list"
6
- * link. Works on the canvas and in the manager.
7
- *
8
- * <VisuallyHidden>Select</VisuallyHidden>
9
- * <VisuallyHidden as="div" aria-live="polite">{{ status }}</VisuallyHidden>
10
- */
11
- withDefaults(defineProps<{
12
- /** The element to draw. */
13
- as?: string | undefined;
14
- /** Show while focused (skip links). */
15
- focusable?: boolean | undefined;
16
- }>(), { as: "span" });
17
- </script>
18
-
19
- <template>
20
- <component :is="as" class="wp-visually-hidden" :data-focusable="focusable || undefined"><slot /></component>
21
- </template>
22
-
23
- <style scoped>
24
- .wp-visually-hidden:not([data-focusable]),
25
- .wp-visually-hidden[data-focusable]:not(:focus):not(:focus-within) {
26
- position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
27
- clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
28
- }
29
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Visually hidden: text for screen readers only (a table header with no
4
+ * visible name, "(opens in a new tab)", a live status line). With
5
+ * `focusable` it shows itself while it has focus, for a "Skip to the list"
6
+ * link. Works on the canvas and in the manager.
7
+ *
8
+ * <VisuallyHidden>Select</VisuallyHidden>
9
+ * <VisuallyHidden as="div" aria-live="polite">{{ status }}</VisuallyHidden>
10
+ */
11
+ withDefaults(defineProps<{
12
+ /** The element to draw. */
13
+ as?: string | undefined;
14
+ /** Show while focused (skip links). */
15
+ focusable?: boolean | undefined;
16
+ }>(), { as: "span" });
17
+ </script>
18
+
19
+ <template>
20
+ <component :is="as" class="wp-visually-hidden" :data-focusable="focusable || undefined"><slot /></component>
21
+ </template>
22
+
23
+ <style scoped>
24
+ .wp-visually-hidden:not([data-focusable]),
25
+ .wp-visually-hidden[data-focusable]:not(:focus):not(:focus-within) {
26
+ position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
27
+ clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
28
+ }
29
+ </style>
package/src/WpButton.vue CHANGED
@@ -1,75 +1,75 @@
1
- <script setup lang="ts">
2
- /**
3
- * The canvas twin of the manager Button (manager/components/ui/Button.vue).
4
- * Same variants, sizes and states, same stylesheet (`button.css`), but the
5
- * icon is a PrimeIcons class, as everywhere on the canvas, and the default
6
- * size is the canvas `sm`.
7
- *
8
- * <WpButton variant="primary" icon="pi-plus">Add module</WpButton>
9
- * <WpButton variant="ghost" icon="pi-download" aria-label="Download snapshot" />
10
- * <WpButton variant="add" icon="pi-plus" block>Add rule</WpButton>
11
- *
12
- * With no slot content it is an icon button (`.wp-btn--icon`); its
13
- * `aria-label` is also its tooltip (`hint` sets another, `kbd` adds a key
14
- * chip). `pressed` turns it into a toggle (a "favourites only" star, a
15
- * filter panel trigger). Loading swaps the icon for the sparkle, sets
16
- * aria-busy and ignores clicks.
17
- *
18
- * The stylesheet is pulled in scoped, so the canvas never carries the
19
- * `.wp-btn` classes globally. Row icon buttons are WpIconButton.
20
- */
21
- import { computed, useSlots } from "vue";
22
- import LoadingMark from "./LoadingMark.vue";
23
- import { vTooltip } from "./tooltip";
24
-
25
- const props = withDefaults(defineProps<{
26
- variant?: "primary" | "secondary" | "ghost" | "danger" | "link" | "outline" | "add" | undefined;
27
- size?: "sm" | "md" | "lg" | undefined;
28
- /** PrimeIcons class (e.g. `pi-plus`), drawn before the label. */
29
- icon?: string | undefined;
30
- loading?: boolean | undefined;
31
- disabled?: boolean | undefined;
32
- /** Stretch to the container's full width. */
33
- block?: boolean | undefined;
34
- /** Makes it a toggle (`aria-pressed`): on, it wears the accent tint. */
35
- pressed?: boolean | undefined;
36
- type?: "button" | "submit" | "reset" | undefined;
37
- ariaLabel?: string | undefined;
38
- /** Tooltip text; an icon button shows its aria-label when this is unset.
39
- * Use this, not `v-tooltip` on the component, which would fight the
40
- * button's own. */
41
- hint?: string | undefined;
42
- /** A shortcut shown in the tooltip's key chip, e.g. "P". */
43
- kbd?: string | undefined;
44
- }>(), { variant: "secondary", size: "sm", icon: undefined, type: "button", ariaLabel: undefined, hint: undefined, kbd: undefined, pressed: undefined });
45
-
46
- const emit = defineEmits<{ click: [evt: MouseEvent] }>();
47
-
48
- const slots = useSlots();
49
- const iconOnly = computed(() => !slots.default);
50
-
51
- function onClick(e: MouseEvent): void {
52
- if (props.disabled || props.loading) return;
53
- emit("click", e);
54
- }
55
- </script>
56
-
57
- <template>
58
- <button
59
- v-tooltip="{ text: hint ?? (iconOnly ? ariaLabel : undefined), kbd }"
60
- :type="type"
61
- class="wp-btn"
62
- :class="[`wp-btn--${variant}`, `wp-btn--${size}`, { 'wp-btn--icon': iconOnly, 'wp-btn--block': block }]"
63
- :disabled="disabled || loading"
64
- :aria-label="ariaLabel"
65
- :aria-busy="loading || undefined"
66
- :aria-pressed="pressed === undefined ? undefined : pressed"
67
- @click="onClick"
68
- >
69
- <LoadingMark v-if="loading" variant="sparkle" mono :size="size === 'lg' ? 16 : size === 'md' ? 14 : 12" decorative />
70
- <i v-else-if="icon" class="pi" :class="icon" aria-hidden="true" />
71
- <slot />
72
- </button>
73
- </template>
74
-
75
- <style scoped src="./button.css"></style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * The canvas twin of the manager Button (manager/components/ui/Button.vue).
4
+ * Same variants, sizes and states, same stylesheet (`button.css`), but the
5
+ * icon is a PrimeIcons class, as everywhere on the canvas, and the default
6
+ * size is the canvas `sm`.
7
+ *
8
+ * <WpButton variant="primary" icon="pi-plus">Add module</WpButton>
9
+ * <WpButton variant="ghost" icon="pi-download" aria-label="Download snapshot" />
10
+ * <WpButton variant="add" icon="pi-plus" block>Add rule</WpButton>
11
+ *
12
+ * With no slot content it is an icon button (`.wp-btn--icon`); its
13
+ * `aria-label` is also its tooltip (`hint` sets another, `kbd` adds a key
14
+ * chip). `pressed` turns it into a toggle (a "favourites only" star, a
15
+ * filter panel trigger). Loading swaps the icon for the sparkle, sets
16
+ * aria-busy and ignores clicks.
17
+ *
18
+ * The stylesheet is pulled in scoped, so the canvas never carries the
19
+ * `.wp-btn` classes globally. Row icon buttons are WpIconButton.
20
+ */
21
+ import { computed, useSlots } from "vue";
22
+ import LoadingMark from "./LoadingMark.vue";
23
+ import { vTooltip } from "./tooltip";
24
+
25
+ const props = withDefaults(defineProps<{
26
+ variant?: "primary" | "secondary" | "ghost" | "danger" | "link" | "outline" | "add" | undefined;
27
+ size?: "sm" | "md" | "lg" | undefined;
28
+ /** PrimeIcons class (e.g. `pi-plus`), drawn before the label. */
29
+ icon?: string | undefined;
30
+ loading?: boolean | undefined;
31
+ disabled?: boolean | undefined;
32
+ /** Stretch to the container's full width. */
33
+ block?: boolean | undefined;
34
+ /** Makes it a toggle (`aria-pressed`): on, it wears the accent tint. */
35
+ pressed?: boolean | undefined;
36
+ type?: "button" | "submit" | "reset" | undefined;
37
+ ariaLabel?: string | undefined;
38
+ /** Tooltip text; an icon button shows its aria-label when this is unset.
39
+ * Use this, not `v-tooltip` on the component, which would fight the
40
+ * button's own. */
41
+ hint?: string | undefined;
42
+ /** A shortcut shown in the tooltip's key chip, e.g. "P". */
43
+ kbd?: string | undefined;
44
+ }>(), { variant: "secondary", size: "sm", icon: undefined, type: "button", ariaLabel: undefined, hint: undefined, kbd: undefined, pressed: undefined });
45
+
46
+ const emit = defineEmits<{ click: [evt: MouseEvent] }>();
47
+
48
+ const slots = useSlots();
49
+ const iconOnly = computed(() => !slots.default);
50
+
51
+ function onClick(e: MouseEvent): void {
52
+ if (props.disabled || props.loading) return;
53
+ emit("click", e);
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <button
59
+ v-tooltip="{ text: hint ?? (iconOnly ? ariaLabel : undefined), kbd }"
60
+ :type="type"
61
+ class="wp-btn"
62
+ :class="[`wp-btn--${variant}`, `wp-btn--${size}`, { 'wp-btn--icon': iconOnly, 'wp-btn--block': block }]"
63
+ :disabled="disabled || loading"
64
+ :aria-label="ariaLabel"
65
+ :aria-busy="loading || undefined"
66
+ :aria-pressed="pressed === undefined ? undefined : pressed"
67
+ @click="onClick"
68
+ >
69
+ <LoadingMark v-if="loading" variant="sparkle" mono :size="size === 'lg' ? 16 : size === 'md' ? 14 : 12" decorative />
70
+ <i v-else-if="icon" class="pi" :class="icon" aria-hidden="true" />
71
+ <slot />
72
+ </button>
73
+ </template>
74
+
75
+ <style scoped src="./button.css"></style>