@dumiflex/wp-kit 0.5.0 → 0.7.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 +47 -0
- package/README.md +5 -0
- package/package.json +4 -3
- package/src/Avatar.vue +1 -1
- package/src/Banner.vue +4 -4
- package/src/BundlePreview.vue +254 -0
- package/src/Button.vue +73 -44
- package/src/Checkbox.vue +201 -63
- package/src/Chip.vue +5 -2
- package/src/CodeBlock.vue +1 -1
- package/src/ColorField.vue +60 -6
- package/src/CombinePreview.vue +150 -0
- package/src/Combobox.vue +10 -14
- package/src/ConfirmModal.vue +12 -22
- package/src/ConstraintMatrixPreview.vue +6 -22
- package/src/ConstraintPreview.vue +259 -0
- package/src/ContextMenu.vue +11 -30
- package/src/CopyButton.vue +1 -1
- package/src/DataTable.vue +0 -2
- package/src/DatePicker.vue +1 -1
- package/src/DerivationCondition.vue +68 -0
- package/src/DerivationPreview.vue +345 -0
- package/src/Disclosure.vue +2 -1
- package/src/Drawer.vue +2 -2
- package/src/DropZone.vue +2 -2
- package/src/FixedValuesPreview.vue +184 -0
- package/src/HsvPicker.vue +2 -2
- package/src/IconPicker.vue +32 -103
- package/src/IconPickerPanel.vue +132 -0
- package/src/ImageGrid.vue +2 -2
- package/src/Lightbox.vue +2 -2
- package/src/LoadingMark.vue +6 -61
- package/src/Modal.vue +109 -44
- package/src/ModalShell.vue +26 -5
- package/src/Pagination.vue +4 -4
- package/src/PayloadPreview.vue +154 -0
- package/src/Popover.vue +68 -12
- package/src/PostDetailSkeleton.vue +65 -0
- package/src/PostGridSkeleton.vue +61 -0
- package/src/PreviewGroup.vue +75 -0
- package/src/ProfileSkeleton.vue +73 -0
- package/src/RadioGroup.vue +5 -4
- package/src/RichText.vue +61 -0
- package/src/SearchInput.vue +1 -1
- package/src/Select.vue +53 -158
- package/src/SelectionBar.vue +90 -0
- package/src/SettingGroup.vue +57 -0
- package/src/SettingRow.vue +127 -0
- package/src/Skeleton.vue +1 -1
- package/src/Slider.vue +166 -55
- package/src/SortableList.vue +1 -1
- package/src/TagInput.vue +1 -1
- package/src/Timeline.vue +2 -2
- package/src/WildcardPreview.vue +607 -0
- package/src/WorkingLine.vue +234 -0
- package/src/WpIconButton.vue +3 -2
- package/src/WpImageZoom.vue +7 -7
- package/src/WpSegmented.vue +3 -1
- package/src/WpSwitch.vue +1 -1
- package/src/WpTabs.vue +1 -1
- package/src/_modal-motion.css +1 -1
- package/src/button.css +8 -6
- package/src/canvas-motion.ts +251 -0
- package/src/floating.ts +5 -0
- package/src/hsv.ts +1 -1
- package/src/kind-colour.ts +24 -1
- package/src/linkify.ts +142 -0
- package/src/loading-mark-motion.ts +12 -155
- package/src/matrix-axis.ts +28 -0
- package/src/menu.css +51 -0
- package/src/motion.css +73 -0
- package/src/pi-icons.ts +51 -0
- package/src/preview-readers.ts +401 -0
- package/src/preview.css +194 -0
- package/src/slider-math.ts +45 -1
- package/src/theme/looks.css +6 -19
- package/src/theme/tokens.css +12 -1108
- package/src/tokens.ts +15 -4
- package/src/tooltip.css +8 -5
- package/src/useDialogFocus.ts +11 -2
- package/src/working-line-motion.ts +135 -0
- package/src/CloseButton.vue +0 -69
- package/src/ColorPicker.vue +0 -201
- package/src/ConfirmDialog.vue +0 -165
- package/src/RangeSlider.vue +0 -183
- package/src/Toggle.vue +0 -36
- package/src/WpButton.vue +0 -75
- package/src/WpCheck.vue +0 -104
- package/src/_skeleton.css +0 -50
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* WorkingLine: "the model is working", in one line like Claude Code's. A
|
|
4
|
+
* small mark drawn from the logo's sparkles, a word that crossfades when it
|
|
5
|
+
* changes, and the time so far. It only draws: the app decides the word,
|
|
6
|
+
* the time and the tempo (the extension's AiWorking cycles playful words
|
|
7
|
+
* and the real status, and reads the request's phase).
|
|
8
|
+
*
|
|
9
|
+
* <WorkingLine word="Thinking" time="12s" tempo="mid" />
|
|
10
|
+
* <WorkingLine :word="word" :word-key="phase" variant="orbit" inline />
|
|
11
|
+
*
|
|
12
|
+
* Marks (`variant`): typing (three stars hop in turn), shuffle (the small
|
|
13
|
+
* stars swap places round the big one), orbit (the logo's four nodes circle
|
|
14
|
+
* the stars) or burst (the small stars fly out of the big one and back).
|
|
15
|
+
* `tempo` sets their speed: slow while the model reads, mid while it thinks,
|
|
16
|
+
* fast while it writes.
|
|
17
|
+
*
|
|
18
|
+
* The word crossfades in place when `wordKey` changes (default: the word),
|
|
19
|
+
* so a token count ticking up can keep its key and not flicker. Its box is
|
|
20
|
+
* never narrower than the word: it widens at once and narrows smoothly once
|
|
21
|
+
* the old word has mostly faded.
|
|
22
|
+
*
|
|
23
|
+
* The marks move on a canvas over the still SVG, so they stay sharp and
|
|
24
|
+
* glide in every browser (`canvas-motion.ts` says why); the SVG is the first
|
|
25
|
+
* paint and stays under reduced motion, where it only breathes.
|
|
26
|
+
*/
|
|
27
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
28
|
+
import { BRAND_COLOURS, STAR_A, STAR_B, STAR_BIG, type LogoBrand } from "./logo-paths";
|
|
29
|
+
import { canvasBox, useCanvasMotion } from "./canvas-motion";
|
|
30
|
+
import { TEMPO_MS, TYPING_X, drawWorking, orbitNodes, type WorkingVariant } from "./working-line-motion";
|
|
31
|
+
|
|
32
|
+
export type { WorkingVariant } from "./working-line-motion";
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<{
|
|
35
|
+
/** What is shown, without the "…" (the line adds it). */
|
|
36
|
+
word: string;
|
|
37
|
+
/** What the crossfade keys on; defaults to the word. */
|
|
38
|
+
wordKey?: string | undefined;
|
|
39
|
+
/** Time so far, already formatted ("42s"). Left out, no time shows. */
|
|
40
|
+
time?: string | undefined;
|
|
41
|
+
variant?: WorkingVariant | undefined;
|
|
42
|
+
tempo?: "slow" | "mid" | "fast" | undefined;
|
|
43
|
+
/** The mark's size in px. */
|
|
44
|
+
size?: number | undefined;
|
|
45
|
+
/** Smaller text and a capped word width, for a top bar. */
|
|
46
|
+
inline?: boolean | undefined;
|
|
47
|
+
/** The whole status, for screen readers and the tooltip; defaults to the word. */
|
|
48
|
+
label?: string | undefined;
|
|
49
|
+
brand?: LogoBrand | undefined;
|
|
50
|
+
}>(), { variant: "typing", tempo: "slow", size: 22, inline: false, brand: "extension" });
|
|
51
|
+
|
|
52
|
+
const key = computed(() => props.wordKey ?? props.word);
|
|
53
|
+
const name = computed(() => props.label ?? props.word);
|
|
54
|
+
|
|
55
|
+
const colours = computed(() => BRAND_COLOURS[props.brand]);
|
|
56
|
+
const BIG = computed(() => colours.value[0]);
|
|
57
|
+
const A = computed(() => colours.value[1]);
|
|
58
|
+
const B = computed(() => colours.value[2]);
|
|
59
|
+
/** The logo's four nodes for the orbit, with steps between the colours. */
|
|
60
|
+
const nodes = computed(() => orbitNodes(colours.value));
|
|
61
|
+
/** Typing stars: the big sparkle drawn small three times in a row. */
|
|
62
|
+
const typing = computed(() => TYPING_X.map((x, i) => [x, [A.value, BIG.value, B.value][i] ?? BIG.value] as const));
|
|
63
|
+
|
|
64
|
+
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
65
|
+
const canvasStyle = computed(() => canvasBox(props.size));
|
|
66
|
+
const { live, drawn } = useCanvasMotion(canvas, (ctx, width, ms) =>
|
|
67
|
+
drawWorking(ctx, width, ms, props.variant, TEMPO_MS[props.tempo], colours.value));
|
|
68
|
+
|
|
69
|
+
const measure = ref<HTMLElement | null>(null);
|
|
70
|
+
const boxWidth = ref<number | null>(null);
|
|
71
|
+
const growing = ref(false);
|
|
72
|
+
async function fit(): Promise<void> {
|
|
73
|
+
await nextTick();
|
|
74
|
+
// Rounded up, plus a pixel to spare: one pixel short is enough for the
|
|
75
|
+
// browser to swap the word's end for its own "…", which Firefox draws
|
|
76
|
+
// invisibly on this gradient text.
|
|
77
|
+
const raw = measure.value?.getBoundingClientRect().width ?? 0;
|
|
78
|
+
const w = raw ? Math.ceil(raw) + 1 : 0;
|
|
79
|
+
if (!w || w === boxWidth.value) return;
|
|
80
|
+
growing.value = boxWidth.value === null || w > boxWidth.value;
|
|
81
|
+
boxWidth.value = w;
|
|
82
|
+
}
|
|
83
|
+
watch(() => props.word, () => { void fit(); });
|
|
84
|
+
|
|
85
|
+
// The word's width can also change on its own, when the font loads late.
|
|
86
|
+
let resized: ResizeObserver | null = null;
|
|
87
|
+
onMounted(() => {
|
|
88
|
+
void fit();
|
|
89
|
+
if (typeof ResizeObserver !== "undefined" && measure.value) {
|
|
90
|
+
resized = new ResizeObserver(() => { void fit(); });
|
|
91
|
+
resized.observe(measure.value);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
onBeforeUnmount(() => resized?.disconnect());
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<div
|
|
99
|
+
class="wp-wl"
|
|
100
|
+
:data-tempo="tempo"
|
|
101
|
+
:data-variant="variant"
|
|
102
|
+
:data-inline="inline ? 'true' : undefined"
|
|
103
|
+
:data-drawn="drawn ? 'true' : undefined"
|
|
104
|
+
role="status"
|
|
105
|
+
:aria-label="name"
|
|
106
|
+
:title="name"
|
|
107
|
+
:style="{ '--wp-wl-big': BIG, '--wp-wl-a': A, '--wp-wl-b': B }"
|
|
108
|
+
>
|
|
109
|
+
<span class="wp-wl__art" :style="{ width: `${size}px`, height: `${size}px` }">
|
|
110
|
+
<svg class="wp-wl__logo" viewBox="0 0 1024 1024" :width="size" :height="size" aria-hidden="true">
|
|
111
|
+
<template v-if="variant === 'typing'">
|
|
112
|
+
<path
|
|
113
|
+
v-for="([x, c], i) in typing" :key="i" :style="{ fill: c }"
|
|
114
|
+
:transform="`translate(${x - 256} 256) scale(.5)`" :d="STAR_BIG"
|
|
115
|
+
/>
|
|
116
|
+
</template>
|
|
117
|
+
<g v-else-if="variant === 'shuffle'" transform="translate(512 512) scale(1.45) translate(-512 -512)">
|
|
118
|
+
<path :style="{ fill: BIG }" :d="STAR_BIG" />
|
|
119
|
+
<path :style="{ fill: A }" :d="STAR_A" />
|
|
120
|
+
<path :style="{ fill: B }" :d="STAR_B" />
|
|
121
|
+
</g>
|
|
122
|
+
<template v-else-if="variant === 'orbit'">
|
|
123
|
+
<circle v-for="([x, y, c], i) in nodes" :key="i" :style="{ fill: c }" :cx="x" :cy="y" r="70" />
|
|
124
|
+
<path :style="{ fill: BIG }" :d="STAR_BIG" />
|
|
125
|
+
<path :style="{ fill: A }" :d="STAR_A" />
|
|
126
|
+
<path :style="{ fill: B }" :d="STAR_B" />
|
|
127
|
+
</template>
|
|
128
|
+
<g v-else transform="translate(512 512) scale(1.3) translate(-512 -512)">
|
|
129
|
+
<path :style="{ fill: BIG }" :d="STAR_BIG" />
|
|
130
|
+
<path :style="{ fill: A }" :d="STAR_A" />
|
|
131
|
+
<path :style="{ fill: B }" :d="STAR_B" />
|
|
132
|
+
</g>
|
|
133
|
+
</svg>
|
|
134
|
+
<canvas v-if="live" ref="canvas" class="wp-wl__canvas" :style="canvasStyle" aria-hidden="true" />
|
|
135
|
+
</span>
|
|
136
|
+
<span
|
|
137
|
+
class="wp-wl__words"
|
|
138
|
+
:data-grow="growing ? 'true' : undefined"
|
|
139
|
+
:style="boxWidth ? { width: `${boxWidth}px` } : undefined"
|
|
140
|
+
>
|
|
141
|
+
<Transition name="wp-wl-word" type="transition">
|
|
142
|
+
<span :key="key" class="wp-wl__word" data-test="working-line-word">{{ word }}…</span>
|
|
143
|
+
</Transition>
|
|
144
|
+
<span ref="measure" class="wp-wl__word wp-wl__measure" aria-hidden="true">{{ word }}…</span>
|
|
145
|
+
</span>
|
|
146
|
+
<span v-if="time !== undefined" class="wp-wl__time" data-test="working-line-time">{{ time }}</span>
|
|
147
|
+
</div>
|
|
148
|
+
</template>
|
|
149
|
+
|
|
150
|
+
<style scoped>
|
|
151
|
+
.wp-wl {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
color: var(--wp-text-muted);
|
|
157
|
+
font-size: 13px;
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
}
|
|
160
|
+
.wp-wl__logo { overflow: visible; }
|
|
161
|
+
.wp-wl__words {
|
|
162
|
+
position: relative;
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
max-width: 100%;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
/* Narrows once the old word has mostly faded; widens at once. */
|
|
169
|
+
transition: width 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) 0.3s;
|
|
170
|
+
}
|
|
171
|
+
.wp-wl__words[data-grow="true"] { transition: none; }
|
|
172
|
+
.wp-wl__word {
|
|
173
|
+
display: inline-block;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
font-weight: 500;
|
|
178
|
+
/* A light band sweeps over the gradient text, like Claude's own line.
|
|
179
|
+
The band is a fixed white, like the logo's brand colours. */
|
|
180
|
+
background:
|
|
181
|
+
linear-gradient(100deg, transparent 35%, rgba(255, 255, 255, 0.75) 50%, transparent 65%) no-repeat,
|
|
182
|
+
linear-gradient(90deg, var(--wp-wl-big), var(--wp-wl-a) 45%, var(--wp-wl-b));
|
|
183
|
+
background-size: 250% 100%, 100% 100%;
|
|
184
|
+
background-position: 150% 0, 0 0;
|
|
185
|
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
186
|
+
animation: wp-wl-shimmer 2.6s linear infinite;
|
|
187
|
+
}
|
|
188
|
+
@keyframes wp-wl-shimmer {
|
|
189
|
+
0% { background-position: 150% 0, 0 0; }
|
|
190
|
+
70%, 100% { background-position: -50% 0, 0 0; }
|
|
191
|
+
}
|
|
192
|
+
/* Sized like the word, never seen: what the box grows or shrinks to. */
|
|
193
|
+
.wp-wl__measure {
|
|
194
|
+
position: absolute; left: 0; top: 0;
|
|
195
|
+
visibility: hidden; pointer-events: none; animation: none;
|
|
196
|
+
max-width: none; overflow: visible;
|
|
197
|
+
}
|
|
198
|
+
.wp-wl__time { flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--wp-text-dim); }
|
|
199
|
+
.wp-wl[data-inline="true"] { font-size: 12px; }
|
|
200
|
+
.wp-wl[data-inline="true"] .wp-wl__words { max-width: 240px; }
|
|
201
|
+
/* Crossfade in place: the old word fades out on top of the new one. The
|
|
202
|
+
* fade and blur run through registered custom properties (see the marks
|
|
203
|
+
* below for why): an `opacity` / `filter` transition lifts the word onto a
|
|
204
|
+
* compositor layer, which re-rasterises the text and snaps it to whole
|
|
205
|
+
* pixels as the fade begins. The leaving word keeps its exact spot (top 0,
|
|
206
|
+
* the box is one word tall), with no -50% translate to land between pixels. */
|
|
207
|
+
.wp-wl-word-enter-active, .wp-wl-word-leave-active {
|
|
208
|
+
opacity: var(--wp-wl-wo);
|
|
209
|
+
filter: blur(var(--wp-wl-wb));
|
|
210
|
+
transition: --wp-wl-wo 0.45s ease, --wp-wl-wb 0.45s ease;
|
|
211
|
+
}
|
|
212
|
+
.wp-wl-word-leave-active { position: absolute; left: 0; top: 0; }
|
|
213
|
+
.wp-wl-word-enter-from, .wp-wl-word-leave-to { --wp-wl-wo: 0; --wp-wl-wb: 2px; }
|
|
214
|
+
@media (prefers-reduced-motion: reduce) {
|
|
215
|
+
.wp-wl__word { animation: none; }
|
|
216
|
+
.wp-wl__words { transition: none; }
|
|
217
|
+
.wp-wl-word-enter-from, .wp-wl-word-leave-to { --wp-wl-wb: 0px; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* The marks move on the canvas; the SVG is the still mark under it. */
|
|
221
|
+
.wp-wl__art { position: relative; display: inline-block; flex-shrink: 0; line-height: 0; }
|
|
222
|
+
.wp-wl__logo { display: block; }
|
|
223
|
+
.wp-wl__canvas { position: absolute; pointer-events: none; }
|
|
224
|
+
.wp-wl[data-drawn="true"] .wp-wl__logo { visibility: hidden; }
|
|
225
|
+
|
|
226
|
+
@property --wp-wl-o { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
227
|
+
@property --wp-wl-wo { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
228
|
+
@property --wp-wl-wb { syntax: "<length>"; inherits: false; initial-value: 0px; }
|
|
229
|
+
|
|
230
|
+
@media (prefers-reduced-motion: reduce) {
|
|
231
|
+
.wp-wl__logo { opacity: var(--wp-wl-o); animation: wp-wl-fade 2.4s ease-in-out infinite; }
|
|
232
|
+
@keyframes wp-wl-fade { 0%, 100% { --wp-wl-o: 1; } 50% { --wp-wl-o: 0.55; } }
|
|
233
|
+
}
|
|
234
|
+
</style>
|
package/src/WpIconButton.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* WpIconButton: the 20 px square icon button inside a canvas row (module,
|
|
4
4
|
* injector, bundle and model rows, the Context toolbar). Rows are too tight
|
|
5
|
-
* for the 24 px `
|
|
5
|
+
* for the 24 px `Button size="sm"`, so this is the one smaller control on
|
|
6
6
|
* the canvas, still at the 20 px hit-target floor.
|
|
7
7
|
*
|
|
8
8
|
* <WpIconButton icon="pi-trash" label="Remove module" tone="danger" @click="remove" />
|
|
@@ -75,7 +75,8 @@ const emit = defineEmits<{ click: [evt: MouseEvent] }>();
|
|
|
75
75
|
.wp-icon-btn[data-tone="accent"] { --tone: var(--wp-accent); }
|
|
76
76
|
.wp-icon-btn[data-tone="warn"] { --tone: var(--wp-warn); }
|
|
77
77
|
.wp-icon-btn[data-tone="danger"] { --tone: var(--wp-danger); }
|
|
78
|
-
|
|
78
|
+
/* Even icon in the 18px inside of the box: an odd size sits half a pixel off. */
|
|
79
|
+
.wp-icon-btn .pi { font-size: 12px; }
|
|
79
80
|
.wp-icon-btn__glyph { font: 600 11px/1 var(--wp-font-mono); }
|
|
80
81
|
.wp-icon-btn:hover:not(:disabled) { background: var(--wp-bg-4); color: var(--tone); }
|
|
81
82
|
.wp-icon-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
package/src/WpImageZoom.vue
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* <WpImageZoom v-model:zoom="zoom" :src="url" alt="Frame 3" />
|
|
26
26
|
*/
|
|
27
27
|
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
28
|
-
import
|
|
28
|
+
import Button from "./Button.vue";
|
|
29
29
|
import {
|
|
30
30
|
centreOf, clampView, fitScale, percent, stepScale, viewAround, visibleRect, zoomAt, IDLE_MS, type Size, type View,
|
|
31
31
|
} from "./image-zoom";
|
|
@@ -298,13 +298,13 @@ const at = (s: number): boolean => !isFit.value && Math.abs(view.value.scale - s
|
|
|
298
298
|
@focusout="chromeLeave"
|
|
299
299
|
>
|
|
300
300
|
<slot name="bar-start" />
|
|
301
|
-
<
|
|
302
|
-
<
|
|
303
|
-
<
|
|
304
|
-
<
|
|
305
|
-
<
|
|
301
|
+
<Button variant="ghost" :size="size" icon="pi-minus" aria-label="Zoom out" kbd="−" data-test="imgzoom-out" @click="zoomOut" />
|
|
302
|
+
<Button :variant="isFit ? 'secondary' : 'ghost'" :size="size" kbd="0" hint="Show the whole picture" :aria-pressed="isFit" data-test="imgzoom-fit" @click="setZoom(null)">Fit</Button>
|
|
303
|
+
<Button :variant="at(1) ? 'secondary' : 'ghost'" :size="size" kbd="1" hint="Real size: one image pixel per screen pixel" :aria-pressed="at(1)" data-test="imgzoom-100" @click="setZoom(1)">100%</Button>
|
|
304
|
+
<Button :variant="at(2) ? 'secondary' : 'ghost'" :size="size" kbd="2" hint="Twice the real size" :aria-pressed="at(2)" data-test="imgzoom-200" @click="setZoom(2)">200%</Button>
|
|
305
|
+
<Button variant="ghost" :size="size" icon="pi-plus" aria-label="Zoom in" kbd="+" data-test="imgzoom-in" @click="zoomIn" />
|
|
306
306
|
<span class="wp-imgzoom__level" aria-live="polite" data-test="imgzoom-level">{{ level }}</span>
|
|
307
|
-
<
|
|
307
|
+
<Button
|
|
308
308
|
v-if="canFull"
|
|
309
309
|
variant="ghost"
|
|
310
310
|
:size="size"
|
package/src/WpSegmented.vue
CHANGED
|
@@ -168,7 +168,9 @@ function onKeydown(ev: KeyboardEvent): void {
|
|
|
168
168
|
.wp-segmented__opt[data-active="true"] .pi { color: var(--wp-accent-text); }
|
|
169
169
|
.wp-segmented__opt:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
170
170
|
.wp-segmented__opt:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
171
|
-
|
|
171
|
+
/* Even sizes: an icon-only option is a square, and an odd icon sits half a pixel off in it. */
|
|
172
|
+
.wp-segmented__opt .pi { font-size: 12px; }
|
|
173
|
+
.wp-segmented[data-size="lg"] .wp-segmented__opt .pi { font-size: 14px; }
|
|
172
174
|
@media (prefers-reduced-motion: reduce) {
|
|
173
175
|
.wp-segmented__opt { transition: none; }
|
|
174
176
|
}
|
package/src/WpSwitch.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* The on/off switch for canvas widgets and the manager (
|
|
3
|
+
* The one on/off switch, for canvas widgets and the manager (size="md" there).
|
|
4
4
|
*
|
|
5
5
|
* <WpSwitch v-model="held" label="Hold across run" /> track
|
|
6
6
|
* <WpSwitch v-model="held" variant="chip" icon="pi-link" label="Hold across run" />
|
package/src/WpTabs.vue
CHANGED
|
@@ -165,7 +165,7 @@ function onKeydown(ev: KeyboardEvent): void {
|
|
|
165
165
|
.wp-tabset__tab[data-active="true"] { color: var(--wp-text); border-bottom-color: var(--wp-accent-400); }
|
|
166
166
|
.wp-tabset__tab:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--wp-border-focus); border-radius: var(--wp-radius-sm); }
|
|
167
167
|
.wp-tabset__tab:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
168
|
-
.wp-tabset__tab .pi { font-size:
|
|
168
|
+
.wp-tabset__tab .pi { font-size: 12px; }
|
|
169
169
|
.wp-tabset__count {
|
|
170
170
|
min-width: 16px;
|
|
171
171
|
padding: 1px 5px;
|
package/src/_modal-motion.css
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the dialog grows into place.
|
|
7
7
|
*
|
|
8
8
|
* Selector for the panel is `> *` (immediate child of the overlay
|
|
9
|
-
* element). Works for ModalShell's `.wp-modal-wrapper
|
|
9
|
+
* element). Works for ModalShell's `.wp-modal-wrapper` (Modal, ConfirmModal), the old
|
|
10
10
|
* `.wp-confirm`, InjectorBindingModal's `.ibm`, and any future modal
|
|
11
11
|
* that follows the overlay-with-one-child layout.
|
|
12
12
|
*
|
package/src/button.css
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
/* Button — the one .wp-btn stylesheet
|
|
2
|
-
*
|
|
3
|
-
* the
|
|
4
|
-
* standard rule 8), which pulls it in scoped. It reads only the
|
|
1
|
+
/* Button — the one .wp-btn stylesheet. Button.vue pulls it in scoped (so it
|
|
2
|
+
* works on the canvas), and the manager and the community site also load it
|
|
3
|
+
* globally for the few plain elements styled as buttons (a router-link). It reads only the
|
|
5
4
|
* SHARED_TOKENS names, so it wears the manager colours on a page and the
|
|
6
5
|
* canvas colours in a node.
|
|
7
6
|
*
|
|
8
7
|
* Unlayered on purpose: the manager loads it beside tokens.css, where an
|
|
9
8
|
* unlayered reset would otherwise beat it. The canvas never loads it
|
|
10
|
-
* globally:
|
|
9
|
+
* globally: Button pulls in a scoped copy, so its classes never meet
|
|
11
10
|
* another extension's `.wp-btn`.
|
|
12
11
|
*
|
|
13
12
|
* Heights follow the control scale: sm 24 / md 32 / lg 40.
|
|
@@ -21,6 +20,8 @@
|
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
22
|
.wp-btn {
|
|
23
|
+
/* A link drawn as a button (<a class="wp-btn">, Button as="a") loses the browser's underline; the link variant adds its own on hover. */
|
|
24
|
+
text-decoration: none;
|
|
24
25
|
height: var(--wp-control-h-md);
|
|
25
26
|
padding: 0 12px;
|
|
26
27
|
border-radius: min(7px, var(--wp-radius-cap, 999px));
|
|
@@ -46,7 +47,8 @@
|
|
|
46
47
|
outline: none;
|
|
47
48
|
box-shadow: var(--wp-focus-ring);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
/* Even icon sizes in the even control heights: an odd one sits half a pixel off. */
|
|
51
|
+
.wp-btn .pi, .wp-btn svg { font-size: 14px; }
|
|
50
52
|
/* The label box (not on a link, which has no fixed height) is trimmed to
|
|
51
53
|
* the cap height where the browser can, so the
|
|
52
54
|
* flex centring puts the capitals, not the ascender-to-descender box, on the
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Looping marks drawn on a canvas (LoadingMark, WorkingLine).
|
|
3
|
+
*
|
|
4
|
+
* A keyframed `transform` / `opacity` on an SVG path goes to the compositor,
|
|
5
|
+
* which blurs the path and snaps it to whole pixels as it starts. Animating
|
|
6
|
+
* registered custom properties fixed that in Chromium, but Firefox still lays
|
|
7
|
+
* each frame of an SVG transform on the pixel grid, so small marks stepped
|
|
8
|
+
* and twitched as they moved. A canvas puts every shape at its exact
|
|
9
|
+
* fractional position each frame, sharp and smooth in both.
|
|
10
|
+
*
|
|
11
|
+
* One `requestAnimationFrame` loop draws every mark on the page. Each mark
|
|
12
|
+
* keeps its own clock, so a host can slow or pause the marks inside it with
|
|
13
|
+
* `data-wp-motion-speed` (a number) and `data-wp-motion-paused` on an
|
|
14
|
+
* ancestor, as the kit site's Animations section does.
|
|
15
|
+
*
|
|
16
|
+
* A mark keeps its still SVG for the first paint, under reduced motion and
|
|
17
|
+
* in a test DOM; `useCanvasMotion` puts the canvas over it once it can draw.
|
|
18
|
+
*/
|
|
19
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, type Ref } from "vue";
|
|
20
|
+
|
|
21
|
+
/** Room around the 1024 box for parts that move past its edge, and for
|
|
22
|
+
* anti-aliasing there. */
|
|
23
|
+
export const PAD = 82;
|
|
24
|
+
export const BOX = 1024 + 2 * PAD;
|
|
25
|
+
|
|
26
|
+
/** CSS `cubic-bezier(x1, y1, x2, y2)` as a function of progress. */
|
|
27
|
+
export function cubicBezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number {
|
|
28
|
+
return (x) => {
|
|
29
|
+
if (x <= 0) return 0;
|
|
30
|
+
if (x >= 1) return 1;
|
|
31
|
+
let lo = 0;
|
|
32
|
+
let hi = 1;
|
|
33
|
+
let t = x;
|
|
34
|
+
for (let k = 0; k < 30; k++) {
|
|
35
|
+
t = (lo + hi) / 2;
|
|
36
|
+
const cx = 3 * x1 * t * (1 - t) ** 2 + 3 * x2 * t * t * (1 - t) + t ** 3;
|
|
37
|
+
if (cx < x) lo = t;
|
|
38
|
+
else hi = t;
|
|
39
|
+
}
|
|
40
|
+
return 3 * y1 * t * (1 - t) ** 2 + 3 * y2 * t * t * (1 - t) + t ** 3;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const EASE = cubicBezier(0.25, 0.1, 0.25, 1);
|
|
45
|
+
export const EASE_IN_OUT = cubicBezier(0.42, 0, 0.58, 1);
|
|
46
|
+
export const SMOOTH = cubicBezier(0.65, 0, 0.35, 1);
|
|
47
|
+
|
|
48
|
+
export type Stop = readonly [at: number, values: readonly number[]];
|
|
49
|
+
|
|
50
|
+
/** Value of a keyframe track at progress `p` (0..1), eased per segment the
|
|
51
|
+
* way CSS eases between two keyframes. */
|
|
52
|
+
export function track(stops: readonly Stop[], p: number, ease: (x: number) => number): number[] {
|
|
53
|
+
for (let k = 0; k < stops.length - 1; k++) {
|
|
54
|
+
const a = stops[k];
|
|
55
|
+
const b = stops[k + 1];
|
|
56
|
+
if (!a || !b) break;
|
|
57
|
+
if (p >= a[0] && p <= b[0]) {
|
|
58
|
+
if (b[0] === a[0]) return [...b[1]];
|
|
59
|
+
const e = ease((p - a[0]) / (b[0] - a[0]));
|
|
60
|
+
return a[1].map((v, j) => v + ((b[1][j] ?? v) - v) * e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...(stops[stops.length - 1]?.[1] ?? [])];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Progress (0..1) through a loop of `period` ms, `ms` into the animation.
|
|
67
|
+
* A positive `delay` holds the start pose until it has passed, like CSS. */
|
|
68
|
+
export function loop(ms: number, period: number, delay = 0): number {
|
|
69
|
+
const t = ms - delay;
|
|
70
|
+
if (t < 0) return 0;
|
|
71
|
+
return (t % period) / period;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Mixes two `#rrggbb` colours in sRGB, like `color-mix(in srgb, a w%, b)`. */
|
|
75
|
+
export function mix(a: string, b: string, weightA: number): string {
|
|
76
|
+
const ch = (hex: string, i: number): number => parseInt(hex.slice(1 + 2 * i, 3 + 2 * i), 16);
|
|
77
|
+
const out = [0, 1, 2].map((i) => Math.round(ch(a, i) * weightA + ch(b, i) * (1 - weightA)));
|
|
78
|
+
return `#${out.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Fills `path` scaled and turned around `[x, y]`. */
|
|
82
|
+
export function fillAround(
|
|
83
|
+
ctx: CanvasRenderingContext2D,
|
|
84
|
+
[x, y]: readonly [number, number],
|
|
85
|
+
scale: number,
|
|
86
|
+
turn: number,
|
|
87
|
+
path: Path2D,
|
|
88
|
+
fill: string | CanvasGradient,
|
|
89
|
+
alpha = 1,
|
|
90
|
+
): void {
|
|
91
|
+
ctx.save();
|
|
92
|
+
ctx.translate(x, y);
|
|
93
|
+
ctx.rotate((turn * Math.PI) / 180);
|
|
94
|
+
ctx.scale(scale, scale);
|
|
95
|
+
ctx.translate(-x, -y);
|
|
96
|
+
ctx.globalAlpha = alpha;
|
|
97
|
+
ctx.fillStyle = fill;
|
|
98
|
+
ctx.fill(path);
|
|
99
|
+
ctx.restore();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Clears the canvas and sets it up to draw in the 1024 box. */
|
|
103
|
+
export function begin(ctx: CanvasRenderingContext2D, width: number): void {
|
|
104
|
+
const k = width / BOX;
|
|
105
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
106
|
+
ctx.clearRect(0, 0, width, width);
|
|
107
|
+
ctx.setTransform(k, 0, 0, k, PAD * k, PAD * k);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Draws one frame `ms` into the animation on a square canvas `width` px wide. */
|
|
111
|
+
export type Draw = (ctx: CanvasRenderingContext2D, width: number, ms: number) => void;
|
|
112
|
+
|
|
113
|
+
/** A mark the loop draws. */
|
|
114
|
+
export interface Animated {
|
|
115
|
+
canvas: HTMLCanvasElement;
|
|
116
|
+
draw: Draw;
|
|
117
|
+
/** ms of animation shown so far, advanced by the loop. */
|
|
118
|
+
clock: number;
|
|
119
|
+
/** False while off screen: the clock still runs, nothing is drawn. */
|
|
120
|
+
visible: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const marks = new Set<Animated>();
|
|
124
|
+
let frame = 0;
|
|
125
|
+
let last = 0;
|
|
126
|
+
|
|
127
|
+
/** Speed and pause from the nearest host that sets them. */
|
|
128
|
+
export function motionOf(el: Element): { speed: number; paused: boolean } {
|
|
129
|
+
const paused = el.closest("[data-wp-motion-paused]") !== null;
|
|
130
|
+
const speedHost = el.closest("[data-wp-motion-speed]");
|
|
131
|
+
const speed = Number(speedHost?.getAttribute("data-wp-motion-speed") ?? "1");
|
|
132
|
+
return { speed: Number.isFinite(speed) && speed >= 0 ? speed : 1, paused };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fit(c: HTMLCanvasElement): void {
|
|
136
|
+
const want = Math.max(1, Math.round(c.clientWidth * (window.devicePixelRatio || 1)));
|
|
137
|
+
if (c.width !== want) {
|
|
138
|
+
c.width = want;
|
|
139
|
+
c.height = want;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function paint(m: Animated): boolean {
|
|
144
|
+
const ctx = m.canvas.getContext("2d");
|
|
145
|
+
if (!ctx) return false;
|
|
146
|
+
fit(m.canvas);
|
|
147
|
+
m.draw(ctx, m.canvas.width, m.clock);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function tick(now: number): void {
|
|
152
|
+
const dt = last ? Math.min(now - last, 100) : 0;
|
|
153
|
+
last = now;
|
|
154
|
+
for (const m of marks) {
|
|
155
|
+
const { speed, paused } = motionOf(m.canvas);
|
|
156
|
+
if (!paused) m.clock += dt * speed;
|
|
157
|
+
if (m.visible) paint(m);
|
|
158
|
+
}
|
|
159
|
+
frame = marks.size ? requestAnimationFrame(tick) : 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Starts drawing a mark; returns the function that stops it. */
|
|
163
|
+
export function animate(mark: Animated): () => void {
|
|
164
|
+
marks.add(mark);
|
|
165
|
+
if (!frame) {
|
|
166
|
+
last = 0;
|
|
167
|
+
frame = requestAnimationFrame(tick);
|
|
168
|
+
}
|
|
169
|
+
return () => {
|
|
170
|
+
marks.delete(mark);
|
|
171
|
+
if (!marks.size && frame) {
|
|
172
|
+
cancelAnimationFrame(frame);
|
|
173
|
+
frame = 0;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Draws one frame right away (before the loop runs, so the mark never
|
|
179
|
+
* shows blank). False when there is no 2D canvas. */
|
|
180
|
+
export function drawNow(mark: Animated): boolean {
|
|
181
|
+
return paint(mark);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Inline style for a canvas over a `px`-wide mark, reaching PAD past it. */
|
|
185
|
+
export function canvasBox(px: number): Record<string, string> {
|
|
186
|
+
const side = (px * BOX) / 1024;
|
|
187
|
+
const off = (px * PAD) / 1024;
|
|
188
|
+
return { width: `${side}px`, height: `${side}px`, top: `${-off}px`, left: `${-off}px` };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Runs a mark's canvas while motion is allowed. Render
|
|
193
|
+
* `<canvas v-if="live" ref="canvas">` over the still SVG, and hide the SVG
|
|
194
|
+
* while `drawn`.
|
|
195
|
+
*/
|
|
196
|
+
export function useCanvasMotion(canvas: Ref<HTMLCanvasElement | null>, draw: Draw): { live: Ref<boolean>; drawn: Ref<boolean> } {
|
|
197
|
+
const live = ref(false);
|
|
198
|
+
const drawn = ref(false);
|
|
199
|
+
let stop: (() => void) | null = null;
|
|
200
|
+
let observer: IntersectionObserver | null = null;
|
|
201
|
+
let motion: MediaQueryList | null = null;
|
|
202
|
+
|
|
203
|
+
async function start(): Promise<void> {
|
|
204
|
+
if (stop) return;
|
|
205
|
+
live.value = true;
|
|
206
|
+
await nextTick();
|
|
207
|
+
const el = canvas.value;
|
|
208
|
+
if (!el || !live.value) return;
|
|
209
|
+
const mark: Animated = { canvas: el, draw, clock: 0, visible: true };
|
|
210
|
+
if (!drawNow(mark)) {
|
|
211
|
+
live.value = false;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
drawn.value = true;
|
|
215
|
+
stop = animate(mark);
|
|
216
|
+
if (typeof IntersectionObserver === "function") {
|
|
217
|
+
observer = new IntersectionObserver((entries) => {
|
|
218
|
+
for (const e of entries) mark.visible = e.isIntersecting;
|
|
219
|
+
});
|
|
220
|
+
observer.observe(el);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function halt(): void {
|
|
224
|
+
stop?.();
|
|
225
|
+
stop = null;
|
|
226
|
+
observer?.disconnect();
|
|
227
|
+
observer = null;
|
|
228
|
+
live.value = false;
|
|
229
|
+
drawn.value = false;
|
|
230
|
+
}
|
|
231
|
+
function onMotionChange(): void {
|
|
232
|
+
if (motion?.matches) halt();
|
|
233
|
+
else void start();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
onMounted(() => {
|
|
237
|
+
// No matchMedia means no way to honour reduced motion, and a test DOM
|
|
238
|
+
// (jsdom) has no canvas and logs an error for every attempt: keep the
|
|
239
|
+
// still SVG in both.
|
|
240
|
+
if (typeof window.matchMedia !== "function" || typeof requestAnimationFrame !== "function") return;
|
|
241
|
+
if (navigator.userAgent.includes("jsdom")) return;
|
|
242
|
+
motion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
243
|
+
motion.addEventListener?.("change", onMotionChange);
|
|
244
|
+
if (!motion.matches) void start();
|
|
245
|
+
});
|
|
246
|
+
onBeforeUnmount(() => {
|
|
247
|
+
motion?.removeEventListener?.("change", onMotionChange);
|
|
248
|
+
halt();
|
|
249
|
+
});
|
|
250
|
+
return { live, drawn };
|
|
251
|
+
}
|
package/src/floating.ts
CHANGED
|
@@ -53,6 +53,11 @@ function room(side: Side, a: Rect, view: Size, gap: number, margin: number): num
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** Space on one side of the anchor for a panel, after the gap and margin. */
|
|
57
|
+
export function roomOn(side: Side, anchor: Rect, view: Size, opts: Pick<PlaceOptions, "gap" | "margin"> = {}): number {
|
|
58
|
+
return room(side, anchor, view, opts.gap ?? 6, opts.margin ?? 8);
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
function clamp(v: number, min: number, max: number): number {
|
|
57
62
|
return max < min ? min : Math.min(Math.max(v, min), max);
|
|
58
63
|
}
|
package/src/hsv.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* lowercase. Functions are tolerant of out-of-range input — they clamp
|
|
6
6
|
* rather than throw, which keeps the picker UI robust under fast drags.
|
|
7
7
|
*
|
|
8
|
-
* Lives in shared/ so both the
|
|
8
|
+
* Lives in shared/ so both the ColorField / TweaksPanel
|
|
9
9
|
* AND the canvas-side bundle modal can drive the same HsvPicker
|
|
10
10
|
* without a cross-package import.
|
|
11
11
|
*/
|
package/src/kind-colour.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The colour of each module type, for a Chip (or anything else) that names
|
|
3
3
|
* one: the `--wp-kind-*` colour the extension uses for that type, bundles
|
|
4
|
-
* slate. There is no kind chip: a kind is a Chip
|
|
4
|
+
* slate, and its icon (`kindIcon`). There is no kind chip: a kind is a Chip
|
|
5
|
+
* with this colour.
|
|
5
6
|
*
|
|
6
7
|
* <Chip :color="kindColour(itemType(post.kind, post.subtype))">{{ itemType(post.kind, post.subtype) }}</Chip>
|
|
7
8
|
*/
|
|
@@ -26,3 +27,25 @@ export function itemType(kind: "module" | "bundle", subtype?: ItemType | null):
|
|
|
26
27
|
export function kindColour(type: ItemType): string {
|
|
27
28
|
return COLOURS[type];
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
/** The PrimeIcons name (for Icon / ListRow `icon`) of each item type: the
|
|
32
|
+
* same icons the extension's kind-icons map uses. */
|
|
33
|
+
const ICONS: Record<ItemType, string> = {
|
|
34
|
+
wildcard: "sparkles",
|
|
35
|
+
fixed_values: "tag",
|
|
36
|
+
combine: "link",
|
|
37
|
+
derivation: "arrow-right-arrow-left",
|
|
38
|
+
constraint: "filter",
|
|
39
|
+
bundle: "box",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The icon of a type read from a payload (any string): `circle` for a type
|
|
43
|
+
* the kit doesn't know. */
|
|
44
|
+
export function kindIcon(type: string): string {
|
|
45
|
+
return (ICONS as Record<string, string>)[type] ?? "circle";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A payload's type string as a module subtype, or null when it isn't one. */
|
|
49
|
+
export function moduleSubtype(type: string): ModuleSubtype | null {
|
|
50
|
+
return type === "fixed_values" || type === "combine" || type === "wildcard" || type === "constraint" || type === "derivation" ? type : null;
|
|
51
|
+
}
|