@dumiflex/wp-kit 0.5.0 → 0.6.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 (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +5 -0
  3. package/package.json +4 -3
  4. package/src/Avatar.vue +1 -1
  5. package/src/Banner.vue +4 -4
  6. package/src/Button.vue +73 -44
  7. package/src/Checkbox.vue +201 -63
  8. package/src/Chip.vue +5 -2
  9. package/src/CodeBlock.vue +1 -1
  10. package/src/ColorField.vue +60 -6
  11. package/src/Combobox.vue +10 -14
  12. package/src/ConfirmModal.vue +12 -22
  13. package/src/ContextMenu.vue +11 -30
  14. package/src/CopyButton.vue +1 -1
  15. package/src/DataTable.vue +0 -2
  16. package/src/DatePicker.vue +1 -1
  17. package/src/Disclosure.vue +2 -1
  18. package/src/Drawer.vue +2 -2
  19. package/src/DropZone.vue +2 -2
  20. package/src/HsvPicker.vue +2 -2
  21. package/src/IconPicker.vue +1 -1
  22. package/src/ImageGrid.vue +2 -2
  23. package/src/Lightbox.vue +2 -2
  24. package/src/Modal.vue +109 -44
  25. package/src/ModalShell.vue +26 -5
  26. package/src/Pagination.vue +4 -4
  27. package/src/Popover.vue +68 -12
  28. package/src/RadioGroup.vue +5 -4
  29. package/src/SearchInput.vue +1 -1
  30. package/src/Select.vue +53 -158
  31. package/src/Skeleton.vue +1 -1
  32. package/src/Slider.vue +166 -55
  33. package/src/SortableList.vue +1 -1
  34. package/src/TagInput.vue +1 -1
  35. package/src/Timeline.vue +2 -2
  36. package/src/WorkingLine.vue +320 -0
  37. package/src/WpIconButton.vue +3 -2
  38. package/src/WpImageZoom.vue +7 -7
  39. package/src/WpSegmented.vue +3 -1
  40. package/src/WpSwitch.vue +1 -1
  41. package/src/WpTabs.vue +1 -1
  42. package/src/_modal-motion.css +1 -1
  43. package/src/button.css +8 -6
  44. package/src/floating.ts +5 -0
  45. package/src/hsv.ts +1 -1
  46. package/src/menu.css +51 -0
  47. package/src/motion.css +73 -0
  48. package/src/slider-math.ts +45 -1
  49. package/src/theme/looks.css +6 -19
  50. package/src/theme/tokens.css +12 -1108
  51. package/src/tokens.ts +8 -3
  52. package/src/tooltip.css +8 -5
  53. package/src/useDialogFocus.ts +11 -2
  54. package/src/CloseButton.vue +0 -69
  55. package/src/ColorPicker.vue +0 -201
  56. package/src/ConfirmDialog.vue +0 -165
  57. package/src/RangeSlider.vue +0 -183
  58. package/src/Toggle.vue +0 -36
  59. package/src/WpButton.vue +0 -75
  60. package/src/WpCheck.vue +0 -104
  61. package/src/_skeleton.css +0 -50
package/src/Modal.vue CHANGED
@@ -1,36 +1,58 @@
1
1
  <script setup lang="ts">
2
- import CloseButton from "./CloseButton.vue";
3
- import { computed, ref, useId, useSlots } from "vue";
2
+ /**
3
+ * Modal: the one dialog, in the manager, on the canvas and on the community
4
+ * site. A titled panel (header with ×, body, optional footer) on ModalShell,
5
+ * so it gets the shared overlay, open/close motion and the key shield that
6
+ * keeps typing away from ComfyUI's shortcuts; useDialogFocus adds the focus
7
+ * trap and focus return.
8
+ *
9
+ * <Modal v-model:open="open" title="Rename module">…<template #footer>…</template></Modal>
10
+ *
11
+ * Escape, the × and the backdrop close it (`close-on-backdrop="false"` keeps
12
+ * a backdrop click from closing a form that would lose work). A custom
13
+ * `#header` puts `:id="titleId"` on its own heading. `keydown` gives every key
14
+ * pressed inside (the shield stops them before `window`), so a dialog's own
15
+ * keys go there, not on a window listener. ConfirmModal is built on it.
16
+ *
17
+ * The panel styles are scoped and read only shared tokens, so it wears the
18
+ * colours of whichever side it is on.
19
+ */
20
+ import { ref, useId, useSlots } from "vue";
21
+ import Button from "./Button.vue";
22
+ import ModalShell from "./ModalShell.vue";
4
23
  import { useDialogFocus } from "./useDialogFocus";
5
24
 
6
- interface Props {
25
+ const props = withDefaults(defineProps<{
7
26
  open: boolean;
8
27
  title?: string | undefined;
9
28
  size?: "sm" | "md" | "lg" | undefined;
10
29
  closeOnBackdrop?: boolean | undefined;
11
- }
12
- const props = withDefaults(defineProps<Props>(), { size: "md", closeOnBackdrop: true });
30
+ }>(), { size: "md", closeOnBackdrop: true, title: undefined });
13
31
 
14
32
  const emit = defineEmits<{
15
33
  (e: "update:open", v: boolean): void;
34
+ /** Every keydown inside the dialog. */
35
+ (e: "keydown", ev: KeyboardEvent): void;
16
36
  }>();
17
37
 
18
38
  const slots = useSlots();
19
-
20
- const modalClasses = computed(() => ["wp-modal", `wp-modal--${props.size}`]);
21
39
  const dialog = ref<HTMLElement | null>(null);
22
40
  const titleId = `wp-modal-title-${useId()}`;
23
41
 
24
- function close() {
42
+ function close(): void {
25
43
  emit("update:open", false);
26
44
  }
27
45
 
28
- function onBackdrop() {
29
- if (props.closeOnBackdrop) close();
46
+ function onShellClose(reason: "escape" | "backdrop"): void {
47
+ if (reason === "backdrop" && !props.closeOnBackdrop) return;
48
+ close();
30
49
  }
31
50
 
32
- useDialogFocus(dialog, () => props.open, {
33
- onEscape: close,
51
+ const focus = useDialogFocus(dialog, () => props.open, {
52
+ // ModalShell closes on Escape; the shield stops keys before window, so the
53
+ // trap runs from the shell's keydown (below), not a window listener.
54
+ onEscape: () => undefined,
55
+ window: false,
34
56
  // The first control in the body, else the dialog itself (the close button
35
57
  // would make Enter close it).
36
58
  initial: (root, els) => {
@@ -38,41 +60,84 @@ useDialogFocus(dialog, () => props.open, {
38
60
  return els.find((el) => body?.contains(el)) ?? root;
39
61
  },
40
62
  });
63
+
64
+ function onKeydown(ev: KeyboardEvent): void {
65
+ focus.onKeydown(ev);
66
+ emit("keydown", ev);
67
+ }
41
68
  </script>
42
69
 
43
70
  <template>
44
- <Teleport to="body">
45
- <!-- The backdrop is a click target only; keyboard users close with Escape. -->
46
- <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
47
- <div
48
- v-if="open"
49
- class="wp-modal__backdrop"
50
- data-test="modal-backdrop"
51
- @mousedown.self="onBackdrop"
52
- >
53
- <div
54
- ref="dialog"
55
- :class="modalClasses"
56
- role="dialog"
57
- aria-modal="true"
58
- :aria-labelledby="title || slots.header ? titleId : undefined"
59
- tabindex="-1"
60
- @mousedown.stop
61
- >
62
- <header v-if="title || slots.header" class="wp-modal__head">
63
- <h3 v-if="title" :id="titleId" class="wp-modal__title">{{ title }}</h3>
64
- <!-- A custom header puts :id="titleId" on its own heading. -->
65
- <slot name="header" :title-id="titleId" />
66
- <span class="wp-spacer" />
67
- <CloseButton kbd="Esc" @click="close" />
68
- </header>
69
- <div class="wp-modal__body">
70
- <slot />
71
- </div>
72
- <footer v-if="slots.footer" class="wp-modal__foot">
73
- <slot name="footer" />
74
- </footer>
71
+ <ModalShell
72
+ :visible="open"
73
+ :labelledby="title || slots.header ? titleId : undefined"
74
+ @close="onShellClose"
75
+ @keydown="onKeydown"
76
+ >
77
+ <div ref="dialog" class="wp-modal" :class="`wp-modal--${size}`" tabindex="-1" data-test="modal">
78
+ <header v-if="title || slots.header" class="wp-modal__head">
79
+ <h3 v-if="title" :id="titleId" class="wp-modal__title">{{ title }}</h3>
80
+ <slot name="header" :title-id="titleId" />
81
+ <span class="wp-modal__spacer" />
82
+ <Button variant="ghost" icon="times" aria-label="Close" kbd="Esc" data-test="modal-close" @click="close" />
83
+ </header>
84
+ <div class="wp-modal__body">
85
+ <slot />
75
86
  </div>
87
+ <footer v-if="slots.footer" class="wp-modal__foot">
88
+ <slot name="footer" />
89
+ </footer>
76
90
  </div>
77
- </Teleport>
91
+ </ModalShell>
78
92
  </template>
93
+
94
+ <style scoped>
95
+ .wp-modal {
96
+ display: flex;
97
+ flex-direction: column;
98
+ width: 100%;
99
+ max-width: 560px;
100
+ max-height: calc(100vh - 48px);
101
+ overflow: hidden;
102
+ background: var(--wp-bg-2);
103
+ border: 1px solid var(--wp-border);
104
+ border-radius: var(--wp-radius-lg);
105
+ box-shadow: var(--wp-shadow-xl);
106
+ color: var(--wp-text);
107
+ font-family: var(--wp-font-sans);
108
+ font-size: var(--wp-text-base);
109
+ }
110
+ /* The dialog takes focus only when nothing in it can. */
111
+ .wp-modal:focus { outline: none; }
112
+ .wp-modal--sm { max-width: 380px; }
113
+ .wp-modal--md { max-width: 560px; }
114
+ .wp-modal--lg { max-width: 880px; }
115
+ .wp-modal__head {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: var(--wp-space-4);
119
+ padding: var(--wp-space-4) var(--wp-space-5);
120
+ border-bottom: 1px solid var(--wp-border);
121
+ background: var(--wp-bg);
122
+ }
123
+ .wp-modal__title { flex: 1; margin: 0; font-size: var(--wp-text-md); font-weight: 600; color: var(--wp-text); }
124
+ .wp-modal__spacer { flex: 1; }
125
+ .wp-modal__title + .wp-modal__spacer { display: none; }
126
+ .wp-modal__body {
127
+ flex: 1;
128
+ min-height: 0;
129
+ overflow: auto;
130
+ padding: var(--wp-space-5);
131
+ }
132
+ .wp-modal__foot {
133
+ display: flex;
134
+ justify-content: flex-end;
135
+ gap: var(--wp-space-3);
136
+ padding: var(--wp-space-4) var(--wp-space-5);
137
+ border-top: 1px solid var(--wp-border);
138
+ background: var(--wp-bg);
139
+ }
140
+ @media (max-width: 640px) {
141
+ .wp-modal__head, .wp-modal__body, .wp-modal__foot { padding-left: var(--wp-space-5); padding-right: var(--wp-space-5); }
142
+ }
143
+ </style>
@@ -3,14 +3,21 @@
3
3
  <Transition name="wp-modal" appear>
4
4
  <!-- The overlay is a click target only; keyboard users close with Escape. -->
5
5
  <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
6
- <div v-if="visible" ref="overlayEl" class="wp-modal-overlay" @click="$emit('close')">
6
+ <div
7
+ v-if="visible"
8
+ ref="overlayEl"
9
+ class="wp-modal-overlay"
10
+ data-test="modal-backdrop"
11
+ @mousedown="downOnBackdrop = isBackdrop($event)"
12
+ @click="onBackdropClick"
13
+ >
7
14
  <div
15
+ ref="wrapperEl"
8
16
  class="wp-modal-wrapper"
9
17
  role="dialog"
10
18
  aria-modal="true"
11
19
  :aria-label="label"
12
20
  :aria-labelledby="labelledby"
13
- @click.stop
14
21
  >
15
22
  <slot></slot>
16
23
  </div>
@@ -31,7 +38,8 @@ const props = defineProps<{
31
38
  labelledby?: string | undefined;
32
39
  }>();
33
40
  const emit = defineEmits<{
34
- (e: "close"): void;
41
+ /** `escape` or `backdrop`, so a dialog can ignore backdrop clicks. */
42
+ (e: "close", reason: "escape" | "backdrop"): void;
35
43
  /** Every keydown inside the modal. Consumers MUST use this instead of a
36
44
  * window listener: the shield stops inside-modal keys at the overlay, so a
37
45
  * window listener would never see them. */
@@ -39,11 +47,24 @@ const emit = defineEmits<{
39
47
  }>();
40
48
 
41
49
  const overlayEl = ref<HTMLElement | null>(null);
50
+ const wrapperEl = ref<HTMLElement | null>(null);
51
+
52
+ /** The overlay or the wrapper around the panel, never the panel's own content. */
53
+ function isBackdrop(ev: MouseEvent): boolean {
54
+ return ev.target === overlayEl.value || ev.target === wrapperEl.value;
55
+ }
56
+ /** Only a press that also STARTED on the backdrop closes: a text selection
57
+ * dragged out of the dialog ends with a click on the overlay. */
58
+ const downOnBackdrop = ref(false);
59
+ function onBackdropClick(ev: MouseEvent): void {
60
+ if (downOnBackdrop.value && isBackdrop(ev)) emit("close", "backdrop");
61
+ downOnBackdrop.value = false;
62
+ }
42
63
 
43
64
  function closeOnEscape(event: KeyboardEvent): void {
44
65
  if (!props.visible || event.key !== "Escape") return;
45
66
  event.preventDefault();
46
- emit("close");
67
+ emit("close", "escape");
47
68
  }
48
69
 
49
70
  // Keys pressed INSIDE the modal are handled here and stop at the overlay, so
@@ -104,6 +125,6 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
104
125
 
105
126
  /* `wp-modal` open/close transition lives in
106
127
  * src/components/shared/_modal-motion.css (imported globally via
107
- * ContextWidget) so every modal — ModalShell consumers, ConfirmDialog,
128
+ * ContextWidget) so every modal — ModalShell consumers (Modal, ConfirmModal),
108
129
  * InjectorBindingModal — animates with the same rules. */
109
130
  </style>
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { computed } from "vue";
15
15
  import { pageItems } from "./pagination";
16
- import WpButton from "./WpButton.vue";
16
+ import Button from "./Button.vue";
17
17
 
18
18
  const props = withDefaults(defineProps<{
19
19
  /** pages: numbered pages. more: a Load more button. */
@@ -54,7 +54,7 @@ const btnSize = computed(() => (props.size === "sm" ? "sm" : "md"));
54
54
  <span class="wp-paging__range" aria-live="polite">{{ from }}–{{ to }} of {{ total }}</span>
55
55
  <ul class="wp-paging__list">
56
56
  <li>
57
- <WpButton variant="ghost" :size="btnSize" icon="pi-chevron-left" aria-label="Previous page" :disabled="disabled || props.page <= 1" @click="go(props.page - 1)" />
57
+ <Button variant="ghost" :size="btnSize" icon="pi-chevron-left" aria-label="Previous page" :disabled="disabled || props.page <= 1" @click="go(props.page - 1)" />
58
58
  </li>
59
59
  <li v-for="it in items" :key="it">
60
60
  <span v-if="typeof it !== 'number'" class="wp-paging__gap" aria-hidden="true">…</span>
@@ -70,13 +70,13 @@ const btnSize = computed(() => (props.size === "sm" ? "sm" : "md"));
70
70
  >{{ it }}</button>
71
71
  </li>
72
72
  <li>
73
- <WpButton variant="ghost" :size="btnSize" icon="pi-chevron-right" aria-label="Next page" :disabled="disabled || props.page >= pages" @click="go(props.page + 1)" />
73
+ <Button variant="ghost" :size="btnSize" icon="pi-chevron-right" aria-label="Next page" :disabled="disabled || props.page >= pages" @click="go(props.page + 1)" />
74
74
  </li>
75
75
  </ul>
76
76
  </nav>
77
77
  <div v-else-if="mode === 'more' && total > 0" class="wp-paging wp-paging--more" :data-size="size">
78
78
  <span class="wp-paging__range" aria-live="polite">Showing {{ Math.min(props.shown, total) }} of {{ total }}{{ noun ? ` ${noun}` : "" }}</span>
79
- <WpButton v-if="props.shown < total" variant="secondary" :size="btnSize" :loading="loading" :disabled="disabled" @click="emit('more')">Load more</WpButton>
79
+ <Button v-if="props.shown < total" variant="secondary" :size="btnSize" :loading="loading" :disabled="disabled" @click="emit('more')">Load more</Button>
80
80
  </div>
81
81
  </template>
82
82
 
package/src/Popover.vue CHANGED
@@ -16,9 +16,15 @@
16
16
  * element don't count as outside, so it can toggle the panel itself.
17
17
  *
18
18
  * <Popover :open="!!cell" :anchor="cell?.el" label="Cell rule" @update:open="cell = null">…</Popover>
19
+ *
20
+ * `variant="menu"` draws the panel as a menu (`menu.css`, the look Select's
21
+ * list, ContextMenu and Combobox share): tighter padding, 4px from the
22
+ * trigger, above modals, and it closes once its trigger scrolls out of view.
23
+ * Every panel is capped at `maxHeight` and at the room on its side, and
24
+ * scrolls past that instead of covering its trigger.
19
25
  */
20
26
  import { computed, nextTick, onBeforeUnmount, ref, useId, watch } from "vue";
21
- import { placeFloating, viewportSize, type Placement, type Side } from "./floating";
27
+ import { placeFloating, roomOn, viewportSize, type Placement, type Side } from "./floating";
22
28
  import { provideFloatingLayer, useFloatingChild } from "./floating-layer";
23
29
 
24
30
  const props = withDefaults(defineProps<{
@@ -34,14 +40,21 @@ const props = withDefaults(defineProps<{
34
40
  role?: "dialog" | "menu" | "listbox" | "group" | undefined;
35
41
  /** Place the panel against this element instead of the trigger slot. */
36
42
  anchor?: HTMLElement | null | undefined;
37
- }>(), { placement: "bottom-start", autofocus: true, role: "dialog", anchor: null });
43
+ /** "menu" for a list of choices (Select, Combobox): the shared menu look. */
44
+ variant?: "panel" | "menu" | undefined;
45
+ /** Tallest the panel grows, in px, before it scrolls. */
46
+ maxHeight?: number | undefined;
47
+ }>(), { placement: "bottom-start", autofocus: true, role: "dialog", anchor: null, variant: "panel" });
38
48
 
39
49
  const emit = defineEmits<{ (e: "update:open", v: boolean): void }>();
40
50
 
41
51
  const id = `wp-popover-${useId()}`;
42
52
  const anchorEl = ref<HTMLElement | null>(null);
43
53
  const panelEl = ref<HTMLElement | null>(null);
44
- const pos = ref<{ x: number; y: number; side: Side; minWidth?: number | undefined }>({ x: -9999, y: -9999, side: "bottom" });
54
+ const pos = ref<{ x: number; y: number; side: Side; minWidth?: number | undefined; maxHeight?: number | undefined }>({ x: -9999, y: -9999, side: "bottom" });
55
+ const isMenu = computed(() => props.variant === "menu");
56
+ /** A short panel still shows a few rows when the room on both sides is tight. */
57
+ const MIN_HEIGHT = 80;
45
58
 
46
59
  const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), '
47
60
  + 'textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
@@ -68,10 +81,39 @@ function place(): void {
68
81
  const p = panelEl.value;
69
82
  if (!t || !p) return;
70
83
  const r = t.getBoundingClientRect();
71
- const at = placeFloating(r, { width: Math.max(p.offsetWidth, props.matchWidth ? r.width : 0), height: p.offsetHeight }, viewportSize(), {
72
- placement: props.placement,
73
- });
74
- pos.value = { ...at, minWidth: props.matchWidth ? r.width : undefined };
84
+ const view = viewportSize();
85
+ const opts = { placement: props.placement, gap: isMenu.value ? 4 : 6 };
86
+ const width = Math.max(p.offsetWidth, props.matchWidth ? r.width : 0);
87
+ // The content's own height (scrollHeight ignores the cap set last time),
88
+ // so a panel that was squeezed can grow back when there is room again.
89
+ const borders = p.offsetHeight - p.clientHeight;
90
+ const natural = Math.min(p.scrollHeight + borders, props.maxHeight ?? Infinity);
91
+ const first = placeFloating(r, { width, height: natural }, view, opts);
92
+ const fits = Math.max(MIN_HEIGHT, roomOn(first.side, r, view, opts));
93
+ const height = Math.min(natural, fits);
94
+ const at = height < natural ? placeFloating(r, { width, height }, view, opts) : first;
95
+ pos.value = {
96
+ ...at,
97
+ minWidth: props.matchWidth ? r.width : undefined,
98
+ maxHeight: Math.min(props.maxHeight ?? Infinity, fits),
99
+ };
100
+ }
101
+
102
+ /** True once no part of the trigger is on screen. */
103
+ function triggerGone(): boolean {
104
+ const t = trigger();
105
+ if (!t) return false;
106
+ const r = t.getBoundingClientRect();
107
+ const view = viewportSize();
108
+ return r.bottom < 0 || r.top > view.height || r.right < 0 || r.left > view.width;
109
+ }
110
+
111
+ function onScroll(ev: Event): void {
112
+ // The panel's own scrolling changes nothing about where it sits.
113
+ if (ev.target instanceof Node && panelEl.value?.contains(ev.target)) return;
114
+ // A menu whose trigger has scrolled away has nothing left to follow.
115
+ if (isMenu.value && triggerGone()) { setOpen(false); return; }
116
+ place();
75
117
  }
76
118
 
77
119
  // A Select list, menu or Popover opened from inside this panel is part of it.
@@ -103,7 +145,7 @@ function listen(on: boolean): void {
103
145
  window[m]("pointerdown", onPointerDown as EventListener, true);
104
146
  window[m]("keydown", onKeydown as EventListener, true);
105
147
  window[m]("resize", place);
106
- window[m]("scroll", place, true);
148
+ window[m]("scroll", onScroll, true);
107
149
  }
108
150
 
109
151
  watch(() => props.open, async (v) => {
@@ -135,17 +177,26 @@ defineExpose({ place });
135
177
  :id="id"
136
178
  ref="panelEl"
137
179
  class="wp-popover"
180
+ :class="{ 'wp-menu': isMenu }"
138
181
  :data-side="pos.side"
182
+ :data-variant="variant"
139
183
  :role="role"
140
184
  :aria-label="label"
141
185
  tabindex="-1"
142
- :style="{ left: `${pos.x}px`, top: `${pos.y}px`, minWidth: pos.minWidth ? `${pos.minWidth}px` : undefined }"
186
+ :style="{
187
+ left: `${pos.x}px`,
188
+ top: `${pos.y}px`,
189
+ minWidth: pos.minWidth ? `${pos.minWidth}px` : undefined,
190
+ maxHeight: pos.maxHeight !== undefined && Number.isFinite(pos.maxHeight) ? `${pos.maxHeight}px` : undefined,
191
+ }"
143
192
  >
144
193
  <slot :close="() => setOpen(false)" />
145
194
  </div>
146
195
  </Teleport>
147
196
  </template>
148
197
 
198
+ <style scoped src="./menu.css"></style>
199
+
149
200
  <style>
150
201
  @layer wp-extension {
151
202
  .wp-popover-anchor { display: inline-flex; }
@@ -155,12 +206,15 @@ defineExpose({ place });
155
206
  max-width: min(420px, calc(100vw - 16px));
156
207
  max-height: calc(100vh - 16px);
157
208
  overflow: auto;
158
- padding: var(--wp-space-3);
209
+ padding: calc(var(--wp-space-3) + 1px); /* plus the 1px the ring stands in for */
159
210
  background: var(--wp-bg-2);
160
211
  color: var(--wp-text);
161
- border: 1px solid var(--wp-border-strong);
212
+ /* The outline is an inset ring, not a border: these rules are layered, and
213
+ a host's unlayered reset (Tailwind 3's preflight sets border-width: 0
214
+ on every element) would beat a border. Nothing resets box-shadow. */
215
+ border: 0;
162
216
  border-radius: var(--wp-radius);
163
- box-shadow: var(--wp-shadow-md);
217
+ box-shadow: inset 0 0 0 1px var(--wp-border-strong), var(--wp-shadow-md);
164
218
  font-family: var(--wp-font-sans);
165
219
  font-size: var(--wp-text-sm);
166
220
  outline: none;
@@ -169,6 +223,8 @@ defineExpose({ place });
169
223
  @keyframes wp-popover-in {
170
224
  from { opacity: 0; transform: translateY(-2px); }
171
225
  }
226
+ /* A menu is as wide as its longest row (up to 620px), not a 420px panel. */
227
+ .wp-popover[data-variant="menu"] { max-width: min(620px, calc(100vw - 16px)); }
172
228
  .wp-popover[data-side="top"] { animation-name: wp-popover-in-up; }
173
229
  @keyframes wp-popover-in-up {
174
230
  from { opacity: 0; transform: translateY(2px); }
@@ -143,16 +143,17 @@ function onKeydown(ev: KeyboardEvent, at: T): void {
143
143
  .wp-radio-group__opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
144
144
  .wp-radio-group__dot {
145
145
  position: relative; flex: none; box-sizing: border-box; width: 16px; height: 16px; margin-top: 1px;
146
- border: 1.5px solid var(--wp-border-strong); border-radius: 50%; /* round: a radio is round in every look */ background: var(--wp-bg-1);
146
+ /* Whole-pixel border and inset, so the dot sits dead centre (a square look shows any half pixel). */
147
+ border: 2px solid var(--wp-border-strong); border-radius: min(50%, var(--wp-radius-cap, 999px)); background: var(--wp-bg-1);
147
148
  transition: border-color var(--wp-motion-hover);
148
149
  }
149
- .wp-radio-group[data-size="sm"] .wp-radio-group__dot { width: 13px; height: 13px; }
150
+ .wp-radio-group[data-size="sm"] .wp-radio-group__dot { width: 12px; height: 12px; border-width: 1px; }
150
151
  .wp-radio-group__opt:hover:not([aria-disabled="true"]) .wp-radio-group__dot { border-color: var(--wp-accent); }
151
152
  .wp-radio-group__opt[data-checked] .wp-radio-group__dot { border-color: var(--wp-accent); }
152
153
  .wp-radio-group__opt[data-checked] .wp-radio-group__dot::after {
153
- content: ""; position: absolute; inset: 3px; border-radius: 50%; /* round: radio dot */ background: var(--wp-accent);
154
+ content: ""; position: absolute; inset: 3px; border-radius: min(50%, var(--wp-radius-cap, 999px)); background: var(--wp-accent);
154
155
  }
155
- .wp-radio-group[data-size="sm"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::after { inset: 2.5px; }
156
+ .wp-radio-group[data-size="sm"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::after { inset: 2px; }
156
157
  /* A picked option that is disabled turns grey, so it can't pass for a live one. */
157
158
  .wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot { border-color: var(--wp-text-dim); }
158
159
  .wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot::after { background: var(--wp-text-dim); }
@@ -186,7 +186,7 @@ defineExpose({
186
186
  color: var(--wp-text-dim);
187
187
  cursor: pointer;
188
188
  }
189
- .wp-search__clear .pi { font-size: 9px; line-height: 1; }
189
+ .wp-search__clear .pi { font-size: 10px; line-height: 1; }
190
190
  .wp-search__clear:hover { color: var(--wp-text); background: var(--wp-bg-3); }
191
191
  .wp-search__clear:focus-visible { outline: none; box-shadow: var(--wp-focus-ring); }
192
192
  </style>