@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.
- package/CHANGELOG.md +92 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +146 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +293 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +98 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/highlight.ts +199 -0
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/loading-mark-motion.ts +249 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
package/src/tokens.ts
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
// The CSS custom properties a kit piece may read.
|
|
2
|
-
//
|
|
3
|
-
// Most pieces also run on the ComfyUI canvas, whose colour sheet is the
|
|
4
|
-
// extension's own (components/shared/theme.css), not src/theme/. Those pieces
|
|
5
|
-
// read only SHARED_TOKENS, which every host defines, so a piece wears
|
|
6
|
-
// whichever side's colours it sits on. The pieces in PAGE_PIECES only run on
|
|
7
|
-
// pages that load src/theme/tokens.css (the manager, the community site, the
|
|
8
|
-
// kit site), so they may read any name that sheet defines.
|
|
9
|
-
// tokens.test.ts holds both rules.
|
|
10
|
-
//
|
|
11
|
-
// Add a shared name only once every host defines it (docs/tokens.md).
|
|
12
|
-
|
|
13
|
-
export const SHARED_TOKENS = [
|
|
14
|
-
// Surfaces and text
|
|
15
|
-
"--wp-bg", "--wp-bg-1", "--wp-bg-2", "--wp-bg-3", "--wp-bg-4",
|
|
16
|
-
"--wp-text", "--wp-text-muted", "--wp-text-dim",
|
|
17
|
-
"--wp-border", "--wp-border-strong", "--wp-border-soft", "--wp-border-focus",
|
|
18
|
-
"--wp-overlay-bg", "--wp-row-hover", "--wp-input-shade", "--wp-scrollbar-thumb",
|
|
19
|
-
// Accent and states
|
|
20
|
-
"--wp-accent", "--wp-accent-glow", "--wp-accent-text", "--wp-accent-text-strong",
|
|
21
|
-
"--wp-accent-300", "--wp-accent-400", "--wp-accent-500", "--wp-accent-600", "--wp-accent-700", "--wp-accent-800",
|
|
22
|
-
"--wp-focus-ring", "--wp-on-accent", "--wp-on-media", "--wp-scrim",
|
|
23
|
-
"--wp-success", "--wp-warn", "--wp-danger", "--wp-info", "--wp-danger-text",
|
|
24
|
-
"--wp-error-border", "--wp-error-text",
|
|
25
|
-
"--wp-red", "--wp-red-bg", "--wp-green", "--wp-green-bg", "--wp-amber", "--wp-amber-bg",
|
|
26
|
-
"--wp-teal", "--wp-teal-bg", "--wp-violet", "--wp-violet-bg", "--wp-rose", "--wp-rose-bg",
|
|
27
|
-
// Kinds and variables
|
|
28
|
-
"--wp-kind-wildcard", "--wp-kind-fixed", "--wp-kind-combine", "--wp-kind-derivation", "--wp-kind-constraint",
|
|
29
|
-
"--wp-var-1", "--wp-var-2", "--wp-var-3", "--wp-var-4", "--wp-var-5", "--wp-var-6", "--wp-var-7", "--wp-var-8",
|
|
30
|
-
// Shape, depth and type
|
|
31
|
-
"--wp-radius-sm", "--wp-radius", "--wp-radius-md", "--wp-radius-lg", "--wp-radius-xl", "--wp-radius-cap",
|
|
32
|
-
"--wp-shadow-sm", "--wp-shadow", "--wp-shadow-md", "--wp-shadow-lg", "--wp-shadow-xl",
|
|
33
|
-
"--wp-font-sans", "--wp-font-mono",
|
|
34
|
-
"--wp-text-xs", "--wp-text-sm", "--wp-text-base", "--wp-text-md", "--wp-text-lg", "--wp-text-xl",
|
|
35
|
-
"--wp-space-0", "--wp-space-1", "--wp-space-2", "--wp-space-3", "--wp-space-4",
|
|
36
|
-
"--wp-space-5", "--wp-space-6", "--wp-space-7", "--wp-space-8",
|
|
37
|
-
"--wp-control-h-sm", "--wp-control-h-md", "--wp-control-h-lg",
|
|
38
|
-
// Motion
|
|
39
|
-
"--wp-motion-quick", "--wp-motion-hover", "--wp-motion-shimmer", "--wp-motion-fade", "--wp-motion-swap", "--wp-motion-flip",
|
|
40
|
-
"--wp-motion-curve-collapse", "--wp-motion-curve-flip", "--wp-motion-curve-linear",
|
|
41
|
-
] as const;
|
|
42
|
-
|
|
43
|
-
/** Read with a fallback (`var(--wp-bg-deep, var(--wp-bg))`) or kept for older
|
|
44
|
-
* hosts; a host may leave these out. */
|
|
45
|
-
export const OPTIONAL_TOKENS = ["--wp-bg-deep", "--wp-bg2"] as const;
|
|
46
|
-
|
|
47
|
-
/** Set by the piece itself, on its own root, not by the host. */
|
|
48
|
-
export const LOCAL_TOKENS = ["--wp-hsv-hue", "--wp-lm-
|
|
49
|
-
|
|
50
|
-
/** Pieces that need a page with src/theme/ loaded (not the canvas). */
|
|
51
|
-
export const PAGE_PIECES = [
|
|
52
|
-
"axis-color.ts", "
|
|
53
|
-
"Button.vue", "Card.vue", "Checkbox.vue", "Chip.vue", "ChoiceCards.vue", "ColorPicker.vue", "ConfirmModal.vue",
|
|
54
|
-
"DataTable.vue", "Drawer.vue", "EmptyState.vue", "Field.vue", "Icon.vue", "IconPicker.vue", "Input.vue",
|
|
55
|
-
"ListFilter.vue", "Modal.vue", "RelativeDate.vue", "Select.vue", "Textarea.vue", "ToastHost.vue", "Toggle.vue",
|
|
56
|
-
] as const;
|
|
1
|
+
// The CSS custom properties a kit piece may read.
|
|
2
|
+
//
|
|
3
|
+
// Most pieces also run on the ComfyUI canvas, whose colour sheet is the
|
|
4
|
+
// extension's own (components/shared/theme.css), not src/theme/. Those pieces
|
|
5
|
+
// read only SHARED_TOKENS, which every host defines, so a piece wears
|
|
6
|
+
// whichever side's colours it sits on. The pieces in PAGE_PIECES only run on
|
|
7
|
+
// pages that load src/theme/tokens.css (the manager, the community site, the
|
|
8
|
+
// kit site), so they may read any name that sheet defines.
|
|
9
|
+
// tokens.test.ts holds both rules.
|
|
10
|
+
//
|
|
11
|
+
// Add a shared name only once every host defines it (docs/tokens.md).
|
|
12
|
+
|
|
13
|
+
export const SHARED_TOKENS = [
|
|
14
|
+
// Surfaces and text
|
|
15
|
+
"--wp-bg", "--wp-bg-1", "--wp-bg-2", "--wp-bg-3", "--wp-bg-4",
|
|
16
|
+
"--wp-text", "--wp-text-muted", "--wp-text-dim",
|
|
17
|
+
"--wp-border", "--wp-border-strong", "--wp-border-soft", "--wp-border-focus",
|
|
18
|
+
"--wp-overlay-bg", "--wp-row-hover", "--wp-input-shade", "--wp-scrollbar-thumb",
|
|
19
|
+
// Accent and states
|
|
20
|
+
"--wp-accent", "--wp-accent-glow", "--wp-accent-text", "--wp-accent-text-strong",
|
|
21
|
+
"--wp-accent-300", "--wp-accent-400", "--wp-accent-500", "--wp-accent-600", "--wp-accent-700", "--wp-accent-800",
|
|
22
|
+
"--wp-focus-ring", "--wp-on-accent", "--wp-on-media", "--wp-scrim",
|
|
23
|
+
"--wp-success", "--wp-warn", "--wp-danger", "--wp-info", "--wp-danger-text",
|
|
24
|
+
"--wp-error-border", "--wp-error-text",
|
|
25
|
+
"--wp-red", "--wp-red-bg", "--wp-green", "--wp-green-bg", "--wp-amber", "--wp-amber-bg",
|
|
26
|
+
"--wp-teal", "--wp-teal-bg", "--wp-violet", "--wp-violet-bg", "--wp-rose", "--wp-rose-bg",
|
|
27
|
+
// Kinds and variables
|
|
28
|
+
"--wp-kind-wildcard", "--wp-kind-fixed", "--wp-kind-combine", "--wp-kind-derivation", "--wp-kind-constraint",
|
|
29
|
+
"--wp-var-1", "--wp-var-2", "--wp-var-3", "--wp-var-4", "--wp-var-5", "--wp-var-6", "--wp-var-7", "--wp-var-8",
|
|
30
|
+
// Shape, depth and type
|
|
31
|
+
"--wp-radius-sm", "--wp-radius", "--wp-radius-md", "--wp-radius-lg", "--wp-radius-xl", "--wp-radius-cap",
|
|
32
|
+
"--wp-shadow-sm", "--wp-shadow", "--wp-shadow-md", "--wp-shadow-lg", "--wp-shadow-xl",
|
|
33
|
+
"--wp-font-sans", "--wp-font-mono",
|
|
34
|
+
"--wp-text-xs", "--wp-text-sm", "--wp-text-base", "--wp-text-md", "--wp-text-lg", "--wp-text-xl",
|
|
35
|
+
"--wp-space-0", "--wp-space-1", "--wp-space-2", "--wp-space-3", "--wp-space-4",
|
|
36
|
+
"--wp-space-5", "--wp-space-6", "--wp-space-7", "--wp-space-8",
|
|
37
|
+
"--wp-control-h-sm", "--wp-control-h-md", "--wp-control-h-lg",
|
|
38
|
+
// Motion
|
|
39
|
+
"--wp-motion-quick", "--wp-motion-hover", "--wp-motion-shimmer", "--wp-motion-fade", "--wp-motion-swap", "--wp-motion-flip",
|
|
40
|
+
"--wp-motion-curve-collapse", "--wp-motion-curve-flip", "--wp-motion-curve-linear",
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
/** Read with a fallback (`var(--wp-bg-deep, var(--wp-bg))`) or kept for older
|
|
44
|
+
* hosts; a host may leave these out. */
|
|
45
|
+
export const OPTIONAL_TOKENS = ["--wp-bg-deep", "--wp-bg2"] as const;
|
|
46
|
+
|
|
47
|
+
/** Set by the piece itself, on its own root, not by the host. */
|
|
48
|
+
export const LOCAL_TOKENS = ["--wp-hsv-hue", "--wp-lm-o", "--wp-select-line-h", "--wp-list-row-depth", "--wp-list-row-icon", "--wp-chip-c", "--wp-field-h"] as const;
|
|
49
|
+
|
|
50
|
+
/** Pieces that need a page with src/theme/ loaded (not the canvas). */
|
|
51
|
+
export const PAGE_PIECES = [
|
|
52
|
+
"axis-color.ts", "kind-colour.ts", "matrix-axis.ts", "BundleMeta.vue", "ConstraintMatrixPreview.vue", "PostCard.vue", "TokenizedValue.vue",
|
|
53
|
+
"Button.vue", "Card.vue", "Checkbox.vue", "Chip.vue", "ChoiceCards.vue", "ColorPicker.vue", "ConfirmModal.vue",
|
|
54
|
+
"DataTable.vue", "Drawer.vue", "EmptyState.vue", "Field.vue", "Icon.vue", "IconPicker.vue", "Input.vue",
|
|
55
|
+
"ListFilter.vue", "Modal.vue", "RelativeDate.vue", "Select.vue", "Textarea.vue", "ToastHost.vue", "Toggle.vue",
|
|
56
|
+
] as const;
|
package/src/tooltip.css
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/* The one shared tooltip element (tooltip.ts). Colours come from tokens both
|
|
2
|
-
* the canvas and the manager define, so it matches whichever side it is on. */
|
|
3
|
-
@layer wp-extension {
|
|
4
|
-
.wp-tooltip {
|
|
5
|
-
position: fixed;
|
|
6
|
-
z-index: 10001;
|
|
7
|
-
display: inline-flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
gap: 8px;
|
|
10
|
-
max-width: 280px;
|
|
11
|
-
padding: 6px 10px;
|
|
12
|
-
border: 1px solid var(--wp-border-strong);
|
|
13
|
-
border-radius: var(--wp-radius);
|
|
14
|
-
background: var(--wp-bg-3);
|
|
15
|
-
color: var(--wp-text);
|
|
16
|
-
box-shadow: var(--wp-shadow-md);
|
|
17
|
-
font: 500 12px/1.4 var(--wp-font-sans);
|
|
18
|
-
pointer-events: none;
|
|
19
|
-
white-space: normal;
|
|
20
|
-
overflow-wrap: break-word;
|
|
21
|
-
opacity: 0;
|
|
22
|
-
visibility: hidden;
|
|
23
|
-
transition: opacity 90ms ease-out, visibility 0s linear 90ms;
|
|
24
|
-
}
|
|
25
|
-
.wp-tooltip[data-open] {
|
|
26
|
-
opacity: 1;
|
|
27
|
-
visibility: visible;
|
|
28
|
-
transition: opacity 90ms ease-out;
|
|
29
|
-
}
|
|
30
|
-
.wp-tooltip__kbd {
|
|
31
|
-
flex-shrink: 0;
|
|
32
|
-
padding: 1px 6px;
|
|
33
|
-
border: 1px solid var(--wp-border-strong);
|
|
34
|
-
border-radius: var(--wp-radius-sm);
|
|
35
|
-
background: var(--wp-bg-1);
|
|
36
|
-
color: var(--wp-text-muted);
|
|
37
|
-
font: 500 11px/16px var(--wp-font-mono);
|
|
38
|
-
white-space: nowrap;
|
|
39
|
-
}
|
|
40
|
-
@media (prefers-reduced-motion: reduce) {
|
|
41
|
-
.wp-tooltip, .wp-tooltip[data-open] { transition: none; }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
/* The one shared tooltip element (tooltip.ts). Colours come from tokens both
|
|
2
|
+
* the canvas and the manager define, so it matches whichever side it is on. */
|
|
3
|
+
@layer wp-extension {
|
|
4
|
+
.wp-tooltip {
|
|
5
|
+
position: fixed;
|
|
6
|
+
z-index: 10001;
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
max-width: 280px;
|
|
11
|
+
padding: 6px 10px;
|
|
12
|
+
border: 1px solid var(--wp-border-strong);
|
|
13
|
+
border-radius: var(--wp-radius);
|
|
14
|
+
background: var(--wp-bg-3);
|
|
15
|
+
color: var(--wp-text);
|
|
16
|
+
box-shadow: var(--wp-shadow-md);
|
|
17
|
+
font: 500 12px/1.4 var(--wp-font-sans);
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
white-space: normal;
|
|
20
|
+
overflow-wrap: break-word;
|
|
21
|
+
opacity: 0;
|
|
22
|
+
visibility: hidden;
|
|
23
|
+
transition: opacity 90ms ease-out, visibility 0s linear 90ms;
|
|
24
|
+
}
|
|
25
|
+
.wp-tooltip[data-open] {
|
|
26
|
+
opacity: 1;
|
|
27
|
+
visibility: visible;
|
|
28
|
+
transition: opacity 90ms ease-out;
|
|
29
|
+
}
|
|
30
|
+
.wp-tooltip__kbd {
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
padding: 1px 6px;
|
|
33
|
+
border: 1px solid var(--wp-border-strong);
|
|
34
|
+
border-radius: var(--wp-radius-sm);
|
|
35
|
+
background: var(--wp-bg-1);
|
|
36
|
+
color: var(--wp-text-muted);
|
|
37
|
+
font: 500 11px/16px var(--wp-font-mono);
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
}
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
.wp-tooltip, .wp-tooltip[data-open] { transition: none; }
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/tooltip.ts
CHANGED
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `v-tooltip`: a short hint shown on hover (after a short wait) and on
|
|
3
|
-
* keyboard focus, hidden on leave, blur or Escape. Use it instead of
|
|
4
|
-
* `title=`, which is slow, unstyled, never shows from the keyboard and is
|
|
5
|
-
* cut off on the canvas.
|
|
6
|
-
*
|
|
7
|
-
* <Button v-tooltip="'Copy the prompt'" icon="pi-copy" aria-label="Copy" />
|
|
8
|
-
* <span v-tooltip="{ text: 'Runs first', placement: 'right' }">…</span>
|
|
9
|
-
* <Button v-tooltip="{ text: 'Copy to clipboard', kbd: 'Ctrl C' }" … />
|
|
10
|
-
*
|
|
11
|
-
* One tooltip element is shared by the whole page, so a thousand hints
|
|
12
|
-
* cost one node. State per element lives in a WeakMap (no expando keys).
|
|
13
|
-
*/
|
|
14
|
-
import type { Directive } from "vue";
|
|
15
|
-
import { placeFloating, viewportSize, type Placement } from "./floating";
|
|
16
|
-
import "./tooltip.css";
|
|
17
|
-
|
|
18
|
-
export type TooltipValue =
|
|
19
|
-
| string
|
|
20
|
-
| null
|
|
21
|
-
| undefined
|
|
22
|
-
| {
|
|
23
|
-
text: string | null | undefined;
|
|
24
|
-
placement?: Placement | undefined;
|
|
25
|
-
/** A shortcut shown after the text, e.g. "Ctrl C". */
|
|
26
|
-
kbd?: string | undefined;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
interface State {
|
|
30
|
-
text: string;
|
|
31
|
-
kbd: string;
|
|
32
|
-
placement: Placement;
|
|
33
|
-
timer: number | undefined;
|
|
34
|
-
off: () => void;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const SHOW_DELAY = 450;
|
|
38
|
-
/** Moving from one hint to the next within this window shows it at once. */
|
|
39
|
-
const WARM_MS = 600;
|
|
40
|
-
|
|
41
|
-
const states = new WeakMap<HTMLElement, State>();
|
|
42
|
-
let tipEl: HTMLDivElement | null = null;
|
|
43
|
-
let owner: HTMLElement | null = null;
|
|
44
|
-
let lastHidden = 0;
|
|
45
|
-
let uid = 0;
|
|
46
|
-
|
|
47
|
-
function read(v: TooltipValue): { text: string; kbd: string; placement: Placement } {
|
|
48
|
-
if (v && typeof v === "object") return { text: v.text ?? "", kbd: v.kbd ?? "", placement: v.placement ?? "top" };
|
|
49
|
-
return { text: v ?? "", kbd: "", placement: "top" };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function tip(): HTMLDivElement {
|
|
53
|
-
if (tipEl && tipEl.isConnected) return tipEl;
|
|
54
|
-
tipEl = document.createElement("div");
|
|
55
|
-
tipEl.className = "wp-tooltip";
|
|
56
|
-
tipEl.id = `wp-tooltip-${++uid}`;
|
|
57
|
-
tipEl.setAttribute("role", "tooltip");
|
|
58
|
-
document.body.appendChild(tipEl);
|
|
59
|
-
return tipEl;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function show(el: HTMLElement): void {
|
|
63
|
-
const s = states.get(el);
|
|
64
|
-
if (!s || !s.text || !el.isConnected) return;
|
|
65
|
-
const t = tip();
|
|
66
|
-
t.textContent = s.text;
|
|
67
|
-
if (s.kbd) {
|
|
68
|
-
const k = document.createElement("kbd");
|
|
69
|
-
k.className = "wp-tooltip__kbd";
|
|
70
|
-
k.textContent = s.kbd;
|
|
71
|
-
t.append(" ", k);
|
|
72
|
-
}
|
|
73
|
-
t.dataset.open = "true";
|
|
74
|
-
owner = el;
|
|
75
|
-
const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter(Boolean);
|
|
76
|
-
if (!ids.includes(t.id)) el.setAttribute("aria-describedby", [...ids, t.id].join(" "));
|
|
77
|
-
const at = placeFloating(el.getBoundingClientRect(), { width: t.offsetWidth, height: t.offsetHeight }, viewportSize(), {
|
|
78
|
-
placement: s.placement,
|
|
79
|
-
});
|
|
80
|
-
t.style.left = `${at.x}px`;
|
|
81
|
-
t.style.top = `${at.y}px`;
|
|
82
|
-
t.dataset.side = at.side;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function hide(el: HTMLElement): void {
|
|
86
|
-
const s = states.get(el);
|
|
87
|
-
if (s?.timer !== undefined) { window.clearTimeout(s.timer); s.timer = undefined; }
|
|
88
|
-
if (owner !== el) return;
|
|
89
|
-
owner = null;
|
|
90
|
-
lastHidden = Date.now();
|
|
91
|
-
if (tipEl) {
|
|
92
|
-
delete tipEl.dataset.open;
|
|
93
|
-
const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter((i) => i && i !== tipEl?.id);
|
|
94
|
-
if (ids.length) el.setAttribute("aria-describedby", ids.join(" "));
|
|
95
|
-
else el.removeAttribute("aria-describedby");
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function schedule(el: HTMLElement): void {
|
|
100
|
-
const s = states.get(el);
|
|
101
|
-
if (!s) return;
|
|
102
|
-
if (s.timer !== undefined) window.clearTimeout(s.timer);
|
|
103
|
-
const warm = owner !== null || Date.now() - lastHidden < WARM_MS;
|
|
104
|
-
if (warm) { show(el); return; }
|
|
105
|
-
s.timer = window.setTimeout(() => { s.timer = undefined; show(el); }, SHOW_DELAY);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export const vTooltip: Directive<HTMLElement, TooltipValue> = {
|
|
109
|
-
mounted(el, binding) {
|
|
110
|
-
const enter = (): void => schedule(el);
|
|
111
|
-
const leave = (): void => hide(el);
|
|
112
|
-
const focus = (): void => { if (el.matches(":focus-visible")) show(el); };
|
|
113
|
-
const key = (ev: KeyboardEvent): void => { if (ev.key === "Escape" && owner === el) hide(el); };
|
|
114
|
-
el.addEventListener("pointerenter", enter);
|
|
115
|
-
el.addEventListener("pointerleave", leave);
|
|
116
|
-
el.addEventListener("pointerdown", leave);
|
|
117
|
-
el.addEventListener("focusin", focus);
|
|
118
|
-
el.addEventListener("focusout", leave);
|
|
119
|
-
el.addEventListener("keydown", key);
|
|
120
|
-
states.set(el, {
|
|
121
|
-
...read(binding.value),
|
|
122
|
-
timer: undefined,
|
|
123
|
-
off: () => {
|
|
124
|
-
el.removeEventListener("pointerenter", enter);
|
|
125
|
-
el.removeEventListener("pointerleave", leave);
|
|
126
|
-
el.removeEventListener("pointerdown", leave);
|
|
127
|
-
el.removeEventListener("focusin", focus);
|
|
128
|
-
el.removeEventListener("focusout", leave);
|
|
129
|
-
el.removeEventListener("keydown", key);
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
},
|
|
133
|
-
updated(el, binding) {
|
|
134
|
-
const s = states.get(el);
|
|
135
|
-
if (!s) return;
|
|
136
|
-
Object.assign(s, read(binding.value));
|
|
137
|
-
if (owner === el) { if (s.text) show(el); else hide(el); }
|
|
138
|
-
},
|
|
139
|
-
beforeUnmount(el) {
|
|
140
|
-
hide(el);
|
|
141
|
-
states.get(el)?.off();
|
|
142
|
-
states.delete(el);
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export default vTooltip;
|
|
1
|
+
/**
|
|
2
|
+
* `v-tooltip`: a short hint shown on hover (after a short wait) and on
|
|
3
|
+
* keyboard focus, hidden on leave, blur or Escape. Use it instead of
|
|
4
|
+
* `title=`, which is slow, unstyled, never shows from the keyboard and is
|
|
5
|
+
* cut off on the canvas.
|
|
6
|
+
*
|
|
7
|
+
* <Button v-tooltip="'Copy the prompt'" icon="pi-copy" aria-label="Copy" />
|
|
8
|
+
* <span v-tooltip="{ text: 'Runs first', placement: 'right' }">…</span>
|
|
9
|
+
* <Button v-tooltip="{ text: 'Copy to clipboard', kbd: 'Ctrl C' }" … />
|
|
10
|
+
*
|
|
11
|
+
* One tooltip element is shared by the whole page, so a thousand hints
|
|
12
|
+
* cost one node. State per element lives in a WeakMap (no expando keys).
|
|
13
|
+
*/
|
|
14
|
+
import type { Directive } from "vue";
|
|
15
|
+
import { placeFloating, viewportSize, type Placement } from "./floating";
|
|
16
|
+
import "./tooltip.css";
|
|
17
|
+
|
|
18
|
+
export type TooltipValue =
|
|
19
|
+
| string
|
|
20
|
+
| null
|
|
21
|
+
| undefined
|
|
22
|
+
| {
|
|
23
|
+
text: string | null | undefined;
|
|
24
|
+
placement?: Placement | undefined;
|
|
25
|
+
/** A shortcut shown after the text, e.g. "Ctrl C". */
|
|
26
|
+
kbd?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface State {
|
|
30
|
+
text: string;
|
|
31
|
+
kbd: string;
|
|
32
|
+
placement: Placement;
|
|
33
|
+
timer: number | undefined;
|
|
34
|
+
off: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SHOW_DELAY = 450;
|
|
38
|
+
/** Moving from one hint to the next within this window shows it at once. */
|
|
39
|
+
const WARM_MS = 600;
|
|
40
|
+
|
|
41
|
+
const states = new WeakMap<HTMLElement, State>();
|
|
42
|
+
let tipEl: HTMLDivElement | null = null;
|
|
43
|
+
let owner: HTMLElement | null = null;
|
|
44
|
+
let lastHidden = 0;
|
|
45
|
+
let uid = 0;
|
|
46
|
+
|
|
47
|
+
function read(v: TooltipValue): { text: string; kbd: string; placement: Placement } {
|
|
48
|
+
if (v && typeof v === "object") return { text: v.text ?? "", kbd: v.kbd ?? "", placement: v.placement ?? "top" };
|
|
49
|
+
return { text: v ?? "", kbd: "", placement: "top" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tip(): HTMLDivElement {
|
|
53
|
+
if (tipEl && tipEl.isConnected) return tipEl;
|
|
54
|
+
tipEl = document.createElement("div");
|
|
55
|
+
tipEl.className = "wp-tooltip";
|
|
56
|
+
tipEl.id = `wp-tooltip-${++uid}`;
|
|
57
|
+
tipEl.setAttribute("role", "tooltip");
|
|
58
|
+
document.body.appendChild(tipEl);
|
|
59
|
+
return tipEl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function show(el: HTMLElement): void {
|
|
63
|
+
const s = states.get(el);
|
|
64
|
+
if (!s || !s.text || !el.isConnected) return;
|
|
65
|
+
const t = tip();
|
|
66
|
+
t.textContent = s.text;
|
|
67
|
+
if (s.kbd) {
|
|
68
|
+
const k = document.createElement("kbd");
|
|
69
|
+
k.className = "wp-tooltip__kbd";
|
|
70
|
+
k.textContent = s.kbd;
|
|
71
|
+
t.append(" ", k);
|
|
72
|
+
}
|
|
73
|
+
t.dataset.open = "true";
|
|
74
|
+
owner = el;
|
|
75
|
+
const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter(Boolean);
|
|
76
|
+
if (!ids.includes(t.id)) el.setAttribute("aria-describedby", [...ids, t.id].join(" "));
|
|
77
|
+
const at = placeFloating(el.getBoundingClientRect(), { width: t.offsetWidth, height: t.offsetHeight }, viewportSize(), {
|
|
78
|
+
placement: s.placement,
|
|
79
|
+
});
|
|
80
|
+
t.style.left = `${at.x}px`;
|
|
81
|
+
t.style.top = `${at.y}px`;
|
|
82
|
+
t.dataset.side = at.side;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hide(el: HTMLElement): void {
|
|
86
|
+
const s = states.get(el);
|
|
87
|
+
if (s?.timer !== undefined) { window.clearTimeout(s.timer); s.timer = undefined; }
|
|
88
|
+
if (owner !== el) return;
|
|
89
|
+
owner = null;
|
|
90
|
+
lastHidden = Date.now();
|
|
91
|
+
if (tipEl) {
|
|
92
|
+
delete tipEl.dataset.open;
|
|
93
|
+
const ids = (el.getAttribute("aria-describedby") ?? "").split(" ").filter((i) => i && i !== tipEl?.id);
|
|
94
|
+
if (ids.length) el.setAttribute("aria-describedby", ids.join(" "));
|
|
95
|
+
else el.removeAttribute("aria-describedby");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function schedule(el: HTMLElement): void {
|
|
100
|
+
const s = states.get(el);
|
|
101
|
+
if (!s) return;
|
|
102
|
+
if (s.timer !== undefined) window.clearTimeout(s.timer);
|
|
103
|
+
const warm = owner !== null || Date.now() - lastHidden < WARM_MS;
|
|
104
|
+
if (warm) { show(el); return; }
|
|
105
|
+
s.timer = window.setTimeout(() => { s.timer = undefined; show(el); }, SHOW_DELAY);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const vTooltip: Directive<HTMLElement, TooltipValue> = {
|
|
109
|
+
mounted(el, binding) {
|
|
110
|
+
const enter = (): void => schedule(el);
|
|
111
|
+
const leave = (): void => hide(el);
|
|
112
|
+
const focus = (): void => { if (el.matches(":focus-visible")) show(el); };
|
|
113
|
+
const key = (ev: KeyboardEvent): void => { if (ev.key === "Escape" && owner === el) hide(el); };
|
|
114
|
+
el.addEventListener("pointerenter", enter);
|
|
115
|
+
el.addEventListener("pointerleave", leave);
|
|
116
|
+
el.addEventListener("pointerdown", leave);
|
|
117
|
+
el.addEventListener("focusin", focus);
|
|
118
|
+
el.addEventListener("focusout", leave);
|
|
119
|
+
el.addEventListener("keydown", key);
|
|
120
|
+
states.set(el, {
|
|
121
|
+
...read(binding.value),
|
|
122
|
+
timer: undefined,
|
|
123
|
+
off: () => {
|
|
124
|
+
el.removeEventListener("pointerenter", enter);
|
|
125
|
+
el.removeEventListener("pointerleave", leave);
|
|
126
|
+
el.removeEventListener("pointerdown", leave);
|
|
127
|
+
el.removeEventListener("focusin", focus);
|
|
128
|
+
el.removeEventListener("focusout", leave);
|
|
129
|
+
el.removeEventListener("keydown", key);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
updated(el, binding) {
|
|
134
|
+
const s = states.get(el);
|
|
135
|
+
if (!s) return;
|
|
136
|
+
Object.assign(s, read(binding.value));
|
|
137
|
+
if (owner === el) { if (s.text) show(el); else hide(el); }
|
|
138
|
+
},
|
|
139
|
+
beforeUnmount(el) {
|
|
140
|
+
hide(el);
|
|
141
|
+
states.get(el)?.off();
|
|
142
|
+
states.delete(el);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default vTooltip;
|
package/src/useDialogFocus.ts
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Focus handling every dialog-like panel needs (Modal, Drawer): while open,
|
|
3
|
-
* Escape calls `onEscape` and Tab stays inside the panel; on open, focus
|
|
4
|
-
* moves in (`initial`, else the first control, else the panel itself); on
|
|
5
|
-
* close, it goes back to where it was.
|
|
6
|
-
*/
|
|
7
|
-
import { nextTick, onBeforeUnmount, watch, type Ref } from "vue";
|
|
8
|
-
|
|
9
|
-
export const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), '
|
|
10
|
-
+ 'textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
11
|
-
|
|
12
|
-
export function focusablesIn(root: HTMLElement | null): HTMLElement[] {
|
|
13
|
-
return Array.from(root?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])
|
|
14
|
-
.filter((el) => !el.closest("[hidden], [inert]"));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface DialogFocusOptions {
|
|
18
|
-
onEscape: () => void;
|
|
19
|
-
/** Where focus goes on open; defaults to the first control. */
|
|
20
|
-
initial?: (root: HTMLElement, controls: HTMLElement[]) => HTMLElement | null | undefined;
|
|
21
|
-
/** Keep Tab inside the panel (default). Off for a panel beside the page. */
|
|
22
|
-
trap?: () => boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function useDialogFocus(panel: Ref<HTMLElement | null>, isOpen: () => boolean, opts: DialogFocusOptions): void {
|
|
26
|
-
let returnTo: HTMLElement | null = null;
|
|
27
|
-
|
|
28
|
-
function onKeydown(e: KeyboardEvent): void {
|
|
29
|
-
if (e.key === "Escape") { opts.onEscape(); return; }
|
|
30
|
-
const root = panel.value;
|
|
31
|
-
if (e.key !== "Tab" || !root || opts.trap?.() === false) return;
|
|
32
|
-
const els = focusablesIn(root);
|
|
33
|
-
if (!els.length) { e.preventDefault(); root.focus(); return; }
|
|
34
|
-
const first = els[0];
|
|
35
|
-
const last = els[els.length - 1];
|
|
36
|
-
const at = document.activeElement;
|
|
37
|
-
const inside = at instanceof Node && root.contains(at);
|
|
38
|
-
if (e.shiftKey && (at === first || !inside)) { e.preventDefault(); last?.focus(); }
|
|
39
|
-
else if (!e.shiftKey && (at === last || !inside)) { e.preventDefault(); first?.focus(); }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function opened(): Promise<void> {
|
|
43
|
-
returnTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
44
|
-
await nextTick();
|
|
45
|
-
const root = panel.value;
|
|
46
|
-
if (!root || root.contains(document.activeElement)) return;
|
|
47
|
-
const els = focusablesIn(root);
|
|
48
|
-
((opts.initial ? opts.initial(root, els) : els[0]) ?? root).focus();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function closed(): void {
|
|
52
|
-
const el = returnTo;
|
|
53
|
-
returnTo = null;
|
|
54
|
-
if (el?.isConnected) el.focus();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
watch(isOpen, (v, was) => {
|
|
58
|
-
if (typeof window === "undefined") return;
|
|
59
|
-
if (v) {
|
|
60
|
-
window.addEventListener("keydown", onKeydown);
|
|
61
|
-
void opened();
|
|
62
|
-
} else {
|
|
63
|
-
window.removeEventListener("keydown", onKeydown);
|
|
64
|
-
if (was) closed();
|
|
65
|
-
}
|
|
66
|
-
}, { immediate: true });
|
|
67
|
-
|
|
68
|
-
onBeforeUnmount(() => {
|
|
69
|
-
if (typeof window === "undefined") return;
|
|
70
|
-
window.removeEventListener("keydown", onKeydown);
|
|
71
|
-
if (isOpen()) closed();
|
|
72
|
-
});
|
|
73
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Focus handling every dialog-like panel needs (Modal, Drawer): while open,
|
|
3
|
+
* Escape calls `onEscape` and Tab stays inside the panel; on open, focus
|
|
4
|
+
* moves in (`initial`, else the first control, else the panel itself); on
|
|
5
|
+
* close, it goes back to where it was.
|
|
6
|
+
*/
|
|
7
|
+
import { nextTick, onBeforeUnmount, watch, type Ref } from "vue";
|
|
8
|
+
|
|
9
|
+
export const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), '
|
|
10
|
+
+ 'textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
11
|
+
|
|
12
|
+
export function focusablesIn(root: HTMLElement | null): HTMLElement[] {
|
|
13
|
+
return Array.from(root?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])
|
|
14
|
+
.filter((el) => !el.closest("[hidden], [inert]"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DialogFocusOptions {
|
|
18
|
+
onEscape: () => void;
|
|
19
|
+
/** Where focus goes on open; defaults to the first control. */
|
|
20
|
+
initial?: (root: HTMLElement, controls: HTMLElement[]) => HTMLElement | null | undefined;
|
|
21
|
+
/** Keep Tab inside the panel (default). Off for a panel beside the page. */
|
|
22
|
+
trap?: () => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useDialogFocus(panel: Ref<HTMLElement | null>, isOpen: () => boolean, opts: DialogFocusOptions): void {
|
|
26
|
+
let returnTo: HTMLElement | null = null;
|
|
27
|
+
|
|
28
|
+
function onKeydown(e: KeyboardEvent): void {
|
|
29
|
+
if (e.key === "Escape") { opts.onEscape(); return; }
|
|
30
|
+
const root = panel.value;
|
|
31
|
+
if (e.key !== "Tab" || !root || opts.trap?.() === false) return;
|
|
32
|
+
const els = focusablesIn(root);
|
|
33
|
+
if (!els.length) { e.preventDefault(); root.focus(); return; }
|
|
34
|
+
const first = els[0];
|
|
35
|
+
const last = els[els.length - 1];
|
|
36
|
+
const at = document.activeElement;
|
|
37
|
+
const inside = at instanceof Node && root.contains(at);
|
|
38
|
+
if (e.shiftKey && (at === first || !inside)) { e.preventDefault(); last?.focus(); }
|
|
39
|
+
else if (!e.shiftKey && (at === last || !inside)) { e.preventDefault(); first?.focus(); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function opened(): Promise<void> {
|
|
43
|
+
returnTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
44
|
+
await nextTick();
|
|
45
|
+
const root = panel.value;
|
|
46
|
+
if (!root || root.contains(document.activeElement)) return;
|
|
47
|
+
const els = focusablesIn(root);
|
|
48
|
+
((opts.initial ? opts.initial(root, els) : els[0]) ?? root).focus();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function closed(): void {
|
|
52
|
+
const el = returnTo;
|
|
53
|
+
returnTo = null;
|
|
54
|
+
if (el?.isConnected) el.focus();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
watch(isOpen, (v, was) => {
|
|
58
|
+
if (typeof window === "undefined") return;
|
|
59
|
+
if (v) {
|
|
60
|
+
window.addEventListener("keydown", onKeydown);
|
|
61
|
+
void opened();
|
|
62
|
+
} else {
|
|
63
|
+
window.removeEventListener("keydown", onKeydown);
|
|
64
|
+
if (was) closed();
|
|
65
|
+
}
|
|
66
|
+
}, { immediate: true });
|
|
67
|
+
|
|
68
|
+
onBeforeUnmount(() => {
|
|
69
|
+
if (typeof window === "undefined") return;
|
|
70
|
+
window.removeEventListener("keydown", onKeydown);
|
|
71
|
+
if (isOpen()) closed();
|
|
72
|
+
});
|
|
73
|
+
}
|