@dumiflex/wp-kit 0.6.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 +15 -0
- package/package.json +1 -1
- package/src/BundlePreview.vue +254 -0
- package/src/CombinePreview.vue +150 -0
- package/src/ConstraintMatrixPreview.vue +6 -22
- package/src/ConstraintPreview.vue +259 -0
- package/src/DerivationCondition.vue +68 -0
- package/src/DerivationPreview.vue +345 -0
- package/src/FixedValuesPreview.vue +184 -0
- package/src/IconPicker.vue +31 -102
- package/src/IconPickerPanel.vue +132 -0
- package/src/LoadingMark.vue +6 -61
- package/src/PayloadPreview.vue +154 -0
- 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/RichText.vue +61 -0
- package/src/SelectionBar.vue +90 -0
- package/src/SettingGroup.vue +57 -0
- package/src/SettingRow.vue +127 -0
- package/src/WildcardPreview.vue +607 -0
- package/src/WorkingLine.vue +38 -124
- package/src/canvas-motion.ts +251 -0
- 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/pi-icons.ts +51 -0
- package/src/preview-readers.ts +401 -0
- package/src/preview.css +194 -0
- package/src/tokens.ts +8 -2
- package/src/working-line-motion.ts +135 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Fixed values as a searchable variable → value → negative table, paged so
|
|
3
|
+
* a long list stays short until asked. */
|
|
4
|
+
import { computed, ref } from "vue";
|
|
5
|
+
import TokenizedValue from "./TokenizedValue.vue";
|
|
6
|
+
import { fixedValues, scanText, type FixedValue, type Obj } from "./preview-readers";
|
|
7
|
+
import SearchInput from "./SearchInput.vue";
|
|
8
|
+
import Button from "./Button.vue";
|
|
9
|
+
import Chip from "./Chip.vue";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
inner: Obj | null;
|
|
13
|
+
refMap: ReadonlyMap<string, string>;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const PAGE = 10;
|
|
17
|
+
const LONG_CHARS = 110;
|
|
18
|
+
|
|
19
|
+
const rows = computed(() => fixedValues(props.inner));
|
|
20
|
+
const negCount = computed(() => rows.value.filter((r) => r.negative).length);
|
|
21
|
+
const refCount = computed(
|
|
22
|
+
() =>
|
|
23
|
+
rows.value.filter((r) => {
|
|
24
|
+
const s = scanText(r.value);
|
|
25
|
+
return s.refs || s.picks;
|
|
26
|
+
}).length,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const search = ref("");
|
|
30
|
+
const onlyNeg = ref(false);
|
|
31
|
+
const filtering = computed(() => search.value.trim() !== "" || onlyNeg.value);
|
|
32
|
+
const visible = computed<FixedValue[]>(() => {
|
|
33
|
+
const q = search.value.trim().toLowerCase();
|
|
34
|
+
return rows.value.filter(
|
|
35
|
+
(r) =>
|
|
36
|
+
(!onlyNeg.value || r.negative) &&
|
|
37
|
+
(!q ||
|
|
38
|
+
r.name.toLowerCase().includes(q) ||
|
|
39
|
+
r.value.toLowerCase().includes(q) ||
|
|
40
|
+
r.negative.toLowerCase().includes(q)),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
const showAll = ref(false);
|
|
44
|
+
const shown = computed(() =>
|
|
45
|
+
showAll.value || filtering.value ? visible.value : visible.value.slice(0, PAGE),
|
|
46
|
+
);
|
|
47
|
+
const expanded = ref(new Set<number>());
|
|
48
|
+
function toggle(i: number): void {
|
|
49
|
+
const next = new Set(expanded.value);
|
|
50
|
+
if (next.has(i)) next.delete(i);
|
|
51
|
+
else next.add(i);
|
|
52
|
+
expanded.value = next;
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div class="wp-pv">
|
|
58
|
+
<div class="wp-pv-stats">
|
|
59
|
+
<div class="wp-pv-stat">
|
|
60
|
+
<b>{{ rows.length }}</b><span>variable{{ rows.length === 1 ? "" : "s" }}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div
|
|
63
|
+
v-if="negCount"
|
|
64
|
+
class="wp-pv-stat"
|
|
65
|
+
>
|
|
66
|
+
<b>{{ negCount }}</b><span>with a negative</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div
|
|
69
|
+
v-if="refCount"
|
|
70
|
+
class="wp-pv-stat"
|
|
71
|
+
>
|
|
72
|
+
<b>{{ refCount }}</b><span>use picks / @refs</span>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div
|
|
76
|
+
v-if="rows.length > 6 || negCount"
|
|
77
|
+
class="wp-pv-tool"
|
|
78
|
+
>
|
|
79
|
+
<SearchInput
|
|
80
|
+
v-if="rows.length > 6"
|
|
81
|
+
v-model="search"
|
|
82
|
+
:placeholder="`Filter ${rows.length} values…`"
|
|
83
|
+
data-test="fixed-search"
|
|
84
|
+
class="wp-pv-search"
|
|
85
|
+
/>
|
|
86
|
+
<Chip
|
|
87
|
+
v-if="negCount"
|
|
88
|
+
size="sm"
|
|
89
|
+
color="var(--wp-danger)"
|
|
90
|
+
:pressed="onlyNeg"
|
|
91
|
+
@click="onlyNeg = !onlyNeg"
|
|
92
|
+
>Has negative</Chip>
|
|
93
|
+
<span
|
|
94
|
+
v-if="filtering"
|
|
95
|
+
class="wp-pv-count"
|
|
96
|
+
>{{ visible.length }} of {{ rows.length }}</span>
|
|
97
|
+
</div>
|
|
98
|
+
<table class="wp-pv-table">
|
|
99
|
+
<colgroup>
|
|
100
|
+
<col class="wp-pv-col-n">
|
|
101
|
+
<col style="width: 150px">
|
|
102
|
+
<col>
|
|
103
|
+
<col
|
|
104
|
+
v-if="negCount"
|
|
105
|
+
style="width: 28%"
|
|
106
|
+
>
|
|
107
|
+
</colgroup>
|
|
108
|
+
<thead>
|
|
109
|
+
<tr>
|
|
110
|
+
<th />
|
|
111
|
+
<th>Variable</th>
|
|
112
|
+
<th>Value</th>
|
|
113
|
+
<th v-if="negCount">
|
|
114
|
+
Negative
|
|
115
|
+
</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
<tr
|
|
120
|
+
v-for="(r, i) in shown"
|
|
121
|
+
:key="i"
|
|
122
|
+
data-test="fixed-row"
|
|
123
|
+
>
|
|
124
|
+
<td class="wp-pv-n">
|
|
125
|
+
{{ i + 1 }}
|
|
126
|
+
</td>
|
|
127
|
+
<td class="wp-pv-var">
|
|
128
|
+
${{ r.name }}
|
|
129
|
+
</td>
|
|
130
|
+
<td>
|
|
131
|
+
<div :class="{ 'wp-pv-clamp': r.value.length > LONG_CHARS && !expanded.has(i) }">
|
|
132
|
+
<TokenizedValue
|
|
133
|
+
:value="r.value"
|
|
134
|
+
:uuid-to-name="refMap"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<Button
|
|
138
|
+
v-if="r.value.length > LONG_CHARS"
|
|
139
|
+
variant="link"
|
|
140
|
+
size="sm"
|
|
141
|
+
@click="toggle(i)"
|
|
142
|
+
>{{ expanded.has(i) ? "Show less" : "Show all" }}</Button>
|
|
143
|
+
</td>
|
|
144
|
+
<td
|
|
145
|
+
v-if="negCount"
|
|
146
|
+
class="wp-pv-neg-cell"
|
|
147
|
+
>
|
|
148
|
+
<span
|
|
149
|
+
v-if="r.negative"
|
|
150
|
+
data-test="fixed-negative"
|
|
151
|
+
>
|
|
152
|
+
<span class="wp-pv-negtag">NEG</span>
|
|
153
|
+
<TokenizedValue
|
|
154
|
+
:value="r.negative"
|
|
155
|
+
:uuid-to-name="refMap"
|
|
156
|
+
/>
|
|
157
|
+
</span>
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
<p
|
|
163
|
+
v-if="!visible.length"
|
|
164
|
+
class="wp-pv-empty"
|
|
165
|
+
>
|
|
166
|
+
No values match.
|
|
167
|
+
</p>
|
|
168
|
+
<div
|
|
169
|
+
v-if="!filtering && visible.length > PAGE"
|
|
170
|
+
class="wp-pv-more"
|
|
171
|
+
>
|
|
172
|
+
<Button
|
|
173
|
+
variant="link"
|
|
174
|
+
size="sm"
|
|
175
|
+
data-test="fixed-more"
|
|
176
|
+
@click="showAll = !showAll"
|
|
177
|
+
>
|
|
178
|
+
{{ showAll ? "Show fewer" : `Show ${visible.length - PAGE} more` }}
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</template>
|
|
183
|
+
|
|
184
|
+
<style scoped src="./preview.css"></style>
|
package/src/IconPicker.vue
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* IconPicker
|
|
3
|
+
* IconPicker: pick one PrimeIcons glyph, or none.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* <IconPicker v-model="cat.icon" /> curated groups in a popover
|
|
6
|
+
* <IconPicker v-model="note.icon" all :color="tint" /> any glyph, with a search box
|
|
7
|
+
* <IconPicker :model-value="null" all inline :allow-none="false" @update:model-value="insert" />
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The closed picker shows the glyph and its name (`compact`: the glyph
|
|
10
|
+
* alone, for a table cell). The list opens in the kit Popover, so it follows
|
|
11
|
+
* the trigger on scroll, flips to stay on screen and closes on Escape or a
|
|
12
|
+
* click outside with focus back on the trigger. `inline` draws the list
|
|
13
|
+
* itself, with no trigger (a panel in an editor). Without `all` the list is
|
|
14
|
+
* the curated groups in `icon-catalog.ts`; with `all` it is every glyph
|
|
15
|
+
* (`pi-icons.ts`, both checked against the installed `primeicons.css` by a
|
|
16
|
+
* test), searchable by name. `color` tints the glyphs; `allow-none` (on by
|
|
17
|
+
* default) offers "No icon", which sets null.
|
|
12
18
|
*
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
19
|
+
* The name is stored WITHOUT the `pi pi-` prefix (`"user"`), which is what
|
|
20
|
+
* Icon.vue expects.
|
|
15
21
|
*/
|
|
16
22
|
import { computed, ref } from "vue";
|
|
23
|
+
import IconPickerPanel from "./IconPickerPanel.vue";
|
|
17
24
|
import Popover from "./Popover.vue";
|
|
18
|
-
import { ICON_GROUPS } from "./icon-catalog";
|
|
19
25
|
|
|
20
26
|
const props = withDefaults(
|
|
21
27
|
defineProps<{
|
|
@@ -25,8 +31,16 @@ const props = withDefaults(
|
|
|
25
31
|
dataTest?: string | undefined;
|
|
26
32
|
/** Icon only, no name beside it, for a tight table cell. */
|
|
27
33
|
compact?: boolean | undefined;
|
|
34
|
+
/** Every PrimeIcons glyph, with a search box, instead of the curated groups. */
|
|
35
|
+
all?: boolean | undefined;
|
|
36
|
+
/** The list itself, no trigger or popover. */
|
|
37
|
+
inline?: boolean | undefined;
|
|
38
|
+
/** Tint the glyphs. */
|
|
39
|
+
color?: string | undefined;
|
|
40
|
+
/** Offer "No icon" (null). */
|
|
41
|
+
allowNone?: boolean | undefined;
|
|
28
42
|
}>(),
|
|
29
|
-
{ ariaLabel: "Pick an icon", dataTest: undefined, compact: false },
|
|
43
|
+
{ ariaLabel: "Pick an icon", dataTest: undefined, compact: false, all: false, inline: false, color: undefined, allowNone: true },
|
|
30
44
|
);
|
|
31
45
|
|
|
32
46
|
const emit = defineEmits<{ "update:modelValue": [v: string | null] }>();
|
|
@@ -49,10 +63,10 @@ function pick(name: string | null): void {
|
|
|
49
63
|
</script>
|
|
50
64
|
|
|
51
65
|
<template>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<Popover v-model:open="open" :label="ariaLabel" role="listbox">
|
|
66
|
+
<div v-if="inline" class="wp-iconpick wp-iconpick--inline" :data-test="dataTest">
|
|
67
|
+
<IconPickerPanel :current="current" :all="all" :color="color" :allow-none="allowNone" @pick="pick" />
|
|
68
|
+
</div>
|
|
69
|
+
<Popover v-else v-model:open="open" :label="ariaLabel" role="listbox">
|
|
56
70
|
<template #trigger="{ props: triggerProps }">
|
|
57
71
|
<button
|
|
58
72
|
v-bind="triggerProps"
|
|
@@ -66,7 +80,7 @@ function pick(name: string | null): void {
|
|
|
66
80
|
different intrinsic widths and baselines, so without it the trigger
|
|
67
81
|
resized and the mark shifted the moment an icon was picked. -->
|
|
68
82
|
<span class="wp-iconpick__slot" aria-hidden="true">
|
|
69
|
-
<i v-if="current" :class="`pi pi-${current}`" />
|
|
83
|
+
<i v-if="current" :class="`pi pi-${current}`" :style="color ? { color } : undefined" />
|
|
70
84
|
<span v-else class="wp-iconpick__none">—</span>
|
|
71
85
|
</span>
|
|
72
86
|
<span
|
|
@@ -78,32 +92,7 @@ function pick(name: string | null): void {
|
|
|
78
92
|
<i class="pi pi-chevron-down wp-iconpick__caret" aria-hidden="true" />
|
|
79
93
|
</button>
|
|
80
94
|
</template>
|
|
81
|
-
<
|
|
82
|
-
<button
|
|
83
|
-
type="button"
|
|
84
|
-
class="wp-iconpick__clear"
|
|
85
|
-
data-test="icon-picker-none"
|
|
86
|
-
@click="pick(null)"
|
|
87
|
-
>No icon</button>
|
|
88
|
-
<div v-for="group in ICON_GROUPS" :key="group.label" class="wp-iconpick__group">
|
|
89
|
-
<div class="wp-iconpick__group-label">{{ group.label }}</div>
|
|
90
|
-
<div class="wp-iconpick__grid">
|
|
91
|
-
<button
|
|
92
|
-
v-for="name in group.icons"
|
|
93
|
-
:key="name"
|
|
94
|
-
type="button"
|
|
95
|
-
class="wp-iconpick__cell"
|
|
96
|
-
:class="{ 'wp-iconpick__cell--on': name === current }"
|
|
97
|
-
role="option"
|
|
98
|
-
:aria-selected="name === current"
|
|
99
|
-
:title="name"
|
|
100
|
-
:aria-label="name"
|
|
101
|
-
:data-test="`icon-picker-${name}`"
|
|
102
|
-
@click="pick(name)"
|
|
103
|
-
><i :class="`pi pi-${name}`" aria-hidden="true" /></button>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
95
|
+
<IconPickerPanel :current="current" :all="all" :color="color" :allow-none="allowNone" :data-test="dataTest" @pick="pick" />
|
|
107
96
|
</Popover>
|
|
108
97
|
</template>
|
|
109
98
|
|
|
@@ -147,63 +136,3 @@ function pick(name: string | null): void {
|
|
|
147
136
|
.wp-iconpick__caret { font-size: 10px; color: var(--wp-text-dim); line-height: 1; }
|
|
148
137
|
</style>
|
|
149
138
|
|
|
150
|
-
<style>
|
|
151
|
-
/* Unscoped: the panel is teleported to <body> by Popover, so a scoped selector's
|
|
152
|
-
`[data-v-…]` attribute would never match it. Class names are
|
|
153
|
-
component-specific (`wp-iconpick*`), so global is safe. */
|
|
154
|
-
/* Lives inside the kit Popover's panel, which gives the frame and shadow. */
|
|
155
|
-
.wp-iconpick__panel {
|
|
156
|
-
width: 212px;
|
|
157
|
-
max-height: 276px;
|
|
158
|
-
overflow-y: auto;
|
|
159
|
-
}
|
|
160
|
-
.wp-iconpick__clear {
|
|
161
|
-
width: 100%;
|
|
162
|
-
margin-bottom: var(--wp-space-3);
|
|
163
|
-
padding: var(--wp-space-2);
|
|
164
|
-
background: transparent;
|
|
165
|
-
border: 1px dashed var(--wp-border);
|
|
166
|
-
border-radius: var(--wp-radius-sm);
|
|
167
|
-
color: var(--wp-text-muted);
|
|
168
|
-
font-size: var(--wp-text-xs);
|
|
169
|
-
cursor: pointer;
|
|
170
|
-
}
|
|
171
|
-
.wp-iconpick__clear:hover { color: var(--wp-text); border-color: var(--wp-accent); }
|
|
172
|
-
.wp-iconpick__group + .wp-iconpick__group { margin-top: var(--wp-space-3); }
|
|
173
|
-
.wp-iconpick__group-label {
|
|
174
|
-
font-size: 9.5px;
|
|
175
|
-
font-weight: 700;
|
|
176
|
-
letter-spacing: 0.08em;
|
|
177
|
-
text-transform: uppercase;
|
|
178
|
-
color: var(--wp-text-dim);
|
|
179
|
-
margin-bottom: var(--wp-space-2);
|
|
180
|
-
}
|
|
181
|
-
.wp-iconpick__grid {
|
|
182
|
-
display: grid;
|
|
183
|
-
grid-template-columns: repeat(8, 1fr);
|
|
184
|
-
gap: 2px;
|
|
185
|
-
}
|
|
186
|
-
.wp-iconpick__cell {
|
|
187
|
-
display: flex;
|
|
188
|
-
align-items: center;
|
|
189
|
-
justify-content: center;
|
|
190
|
-
aspect-ratio: 1;
|
|
191
|
-
background: transparent;
|
|
192
|
-
border: 1px solid transparent;
|
|
193
|
-
border-radius: var(--wp-radius-sm);
|
|
194
|
-
color: var(--wp-text-muted);
|
|
195
|
-
cursor: pointer;
|
|
196
|
-
font-size: 13px;
|
|
197
|
-
}
|
|
198
|
-
/* Kill the icon font's own line box — it otherwise pushes the glyph off the
|
|
199
|
-
* cell's vertical centre even though the cell is a centred flex container. */
|
|
200
|
-
.wp-iconpick__cell i { line-height: 1; }
|
|
201
|
-
.wp-iconpick__cell:hover {
|
|
202
|
-
background: color-mix(in oklab, var(--wp-accent) 18%, transparent);
|
|
203
|
-
color: var(--wp-text);
|
|
204
|
-
}
|
|
205
|
-
.wp-iconpick__cell--on {
|
|
206
|
-
border-color: var(--wp-accent);
|
|
207
|
-
color: var(--wp-accent);
|
|
208
|
-
}
|
|
209
|
-
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The grid inside IconPicker (in its Popover, or on its own with `inline`).
|
|
4
|
+
* Internal: use IconPicker. Without `all` it offers the curated groups in
|
|
5
|
+
* `icon-catalog.ts`; with `all`, every PrimeIcons glyph (`pi-icons.ts`) and a
|
|
6
|
+
* search box. Emits the picked name without `pi-`, or null for No icon.
|
|
7
|
+
*/
|
|
8
|
+
import { computed, ref } from "vue";
|
|
9
|
+
import SearchInput from "./SearchInput.vue";
|
|
10
|
+
import { vTooltip } from "./tooltip";
|
|
11
|
+
import { ICON_GROUPS } from "./icon-catalog";
|
|
12
|
+
import { PI_ICONS } from "./pi-icons";
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<{
|
|
15
|
+
current: string | null;
|
|
16
|
+
all?: boolean | undefined;
|
|
17
|
+
/** Tint the glyphs (a note's colour). */
|
|
18
|
+
color?: string | undefined;
|
|
19
|
+
allowNone?: boolean | undefined;
|
|
20
|
+
dataTest?: string | undefined;
|
|
21
|
+
}>(), { all: false, color: undefined, allowNone: true, dataTest: undefined });
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{ pick: [name: string | null] }>();
|
|
24
|
+
|
|
25
|
+
const query = ref("");
|
|
26
|
+
const words = (name: string): string => name.replace(/-/g, " ");
|
|
27
|
+
/** "arrow right" and "pi-arrow-right" both find arrow-right. */
|
|
28
|
+
const found = computed(() => {
|
|
29
|
+
const q = query.value.trim().toLowerCase().replace(/^pi-/, "").replace(/\s+/g, "-");
|
|
30
|
+
return q ? PI_ICONS.filter((n) => n.includes(q)) : PI_ICONS;
|
|
31
|
+
});
|
|
32
|
+
const groups = computed(() => (props.all ? [{ label: "", icons: found.value }] : ICON_GROUPS));
|
|
33
|
+
const tint = computed(() => (props.color ? { color: props.color } : undefined));
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="wp-iconpick__panel" :data-all="all || undefined" :data-test="dataTest ? `${dataTest}-panel` : undefined">
|
|
38
|
+
<SearchInput
|
|
39
|
+
v-if="all"
|
|
40
|
+
v-model="query"
|
|
41
|
+
size="sm"
|
|
42
|
+
placeholder="Search icons…"
|
|
43
|
+
aria-label="Search icons"
|
|
44
|
+
test-prefix="icon-picker"
|
|
45
|
+
class="wp-iconpick__search"
|
|
46
|
+
/>
|
|
47
|
+
<button
|
|
48
|
+
v-if="allowNone"
|
|
49
|
+
type="button"
|
|
50
|
+
class="wp-iconpick__clear"
|
|
51
|
+
:aria-pressed="!current"
|
|
52
|
+
data-test="icon-picker-none"
|
|
53
|
+
@click="emit('pick', null)"
|
|
54
|
+
>No icon</button>
|
|
55
|
+
<div v-for="group in groups" :key="group.label" class="wp-iconpick__group">
|
|
56
|
+
<div v-if="group.label" class="wp-iconpick__group-label">{{ group.label }}</div>
|
|
57
|
+
<div class="wp-iconpick__grid" role="listbox" :aria-label="group.label || 'Icons'">
|
|
58
|
+
<button
|
|
59
|
+
v-for="name in group.icons"
|
|
60
|
+
:key="name"
|
|
61
|
+
v-tooltip="words(name)"
|
|
62
|
+
type="button"
|
|
63
|
+
class="wp-iconpick__cell"
|
|
64
|
+
:class="{ 'wp-iconpick__cell--on': name === current }"
|
|
65
|
+
role="option"
|
|
66
|
+
:aria-selected="name === current"
|
|
67
|
+
:aria-label="words(name)"
|
|
68
|
+
:data-test="`icon-picker-${name}`"
|
|
69
|
+
@click="emit('pick', name)"
|
|
70
|
+
><i :class="`pi pi-${name}`" :style="tint" aria-hidden="true" /></button>
|
|
71
|
+
<p v-if="!group.icons.length" class="wp-iconpick__empty">No icon matches "{{ query }}".</p>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.wp-iconpick__panel {
|
|
79
|
+
width: 212px;
|
|
80
|
+
max-height: 276px;
|
|
81
|
+
overflow-y: auto;
|
|
82
|
+
}
|
|
83
|
+
.wp-iconpick__panel[data-all] { width: 264px; }
|
|
84
|
+
/* Stays at the top while the glyphs scroll under it. */
|
|
85
|
+
.wp-iconpick__search { position: sticky; top: 0; z-index: 1; margin-bottom: var(--wp-space-3); box-shadow: 0 0 0 var(--wp-space-2) var(--wp-bg-2); background: var(--wp-bg-2); }
|
|
86
|
+
.wp-iconpick__clear {
|
|
87
|
+
width: 100%;
|
|
88
|
+
margin-bottom: var(--wp-space-3);
|
|
89
|
+
padding: var(--wp-space-2);
|
|
90
|
+
background: transparent;
|
|
91
|
+
border: 1px dashed var(--wp-border);
|
|
92
|
+
border-radius: var(--wp-radius-sm);
|
|
93
|
+
color: var(--wp-text-muted);
|
|
94
|
+
font: inherit;
|
|
95
|
+
font-size: var(--wp-text-xs);
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
.wp-iconpick__clear:hover { color: var(--wp-text); border-color: var(--wp-accent); }
|
|
99
|
+
.wp-iconpick__clear[aria-pressed="true"] { border-style: solid; border-color: var(--wp-accent); color: var(--wp-text); }
|
|
100
|
+
.wp-iconpick__clear:focus-visible, .wp-iconpick__cell:focus-visible { outline: none; box-shadow: var(--wp-focus-ring); }
|
|
101
|
+
.wp-iconpick__group + .wp-iconpick__group { margin-top: var(--wp-space-3); }
|
|
102
|
+
.wp-iconpick__group-label {
|
|
103
|
+
margin-bottom: var(--wp-space-2);
|
|
104
|
+
font-size: var(--wp-text-xs);
|
|
105
|
+
font-weight: 700;
|
|
106
|
+
letter-spacing: 0.08em;
|
|
107
|
+
text-transform: uppercase;
|
|
108
|
+
color: var(--wp-text-dim);
|
|
109
|
+
}
|
|
110
|
+
.wp-iconpick__grid {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: repeat(8, 1fr);
|
|
113
|
+
gap: 2px;
|
|
114
|
+
}
|
|
115
|
+
.wp-iconpick__cell {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
aspect-ratio: 1;
|
|
120
|
+
padding: 0;
|
|
121
|
+
background: transparent;
|
|
122
|
+
border: 1px solid transparent;
|
|
123
|
+
border-radius: var(--wp-radius-sm);
|
|
124
|
+
color: var(--wp-text-muted);
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
}
|
|
127
|
+
/* An even size, on whole pixels; the font's own line box would push it off centre. */
|
|
128
|
+
.wp-iconpick__cell .pi { font-size: 14px; line-height: 1; }
|
|
129
|
+
.wp-iconpick__cell:hover { background: color-mix(in oklab, var(--wp-accent) 18%, transparent); color: var(--wp-text); }
|
|
130
|
+
.wp-iconpick__cell--on { border-color: var(--wp-accent); color: var(--wp-accent); }
|
|
131
|
+
.wp-iconpick__empty { grid-column: 1 / -1; margin: var(--wp-space-2) 0; color: var(--wp-text-dim); font-size: var(--wp-text-sm); }
|
|
132
|
+
</style>
|
package/src/LoadingMark.vue
CHANGED
|
@@ -21,9 +21,10 @@ let nextId = 0;
|
|
|
21
21
|
* box, for a panel or page that is loading. `mono` draws it in the text
|
|
22
22
|
* colour, for a host with its own colours (a busy button).
|
|
23
23
|
*/
|
|
24
|
-
import { computed,
|
|
24
|
+
import { computed, ref, useSlots } from "vue";
|
|
25
25
|
import { BRAND_COLOURS, RING_A, RING_B, STAR_A, STAR_B, STAR_BIG, type LogoBrand } from "./logo-paths";
|
|
26
|
-
import {
|
|
26
|
+
import { canvasBox, useCanvasMotion } from "./canvas-motion";
|
|
27
|
+
import { drawPose, poseAt, type Look } from "./loading-mark-motion";
|
|
27
28
|
|
|
28
29
|
const props = withDefaults(defineProps<{
|
|
29
30
|
/** Width and height in px. Default 14, or 40 with `block`. */
|
|
@@ -56,16 +57,10 @@ const fill = (colour: string): string => (props.mono ? "currentColor" : colour);
|
|
|
56
57
|
|
|
57
58
|
/* The SVG is the still mark (first paint, reduced motion, a page with no
|
|
58
59
|
* canvas). Once mounted, a canvas over it draws the motion: see
|
|
59
|
-
* `
|
|
60
|
+
* `canvas-motion.ts` for why it is not CSS. */
|
|
60
61
|
const root = ref<HTMLElement | null>(null);
|
|
61
62
|
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
62
|
-
const
|
|
63
|
-
const drawn = ref(false);
|
|
64
|
-
const canvasStyle = computed(() => {
|
|
65
|
-
const side = (px.value * BOX) / 1024;
|
|
66
|
-
const off = (px.value * PAD) / 1024;
|
|
67
|
-
return { width: `${side}px`, height: `${side}px`, top: `${-off}px`, left: `${-off}px` };
|
|
68
|
-
});
|
|
63
|
+
const canvasStyle = computed(() => canvasBox(px.value));
|
|
69
64
|
|
|
70
65
|
let textColour = "";
|
|
71
66
|
let textColourAt = -Infinity;
|
|
@@ -81,57 +76,7 @@ function look(): Look {
|
|
|
81
76
|
return { variant: drawing.value, colours: [VIOLET.value, INDIGO.value, TEAL.value], flat: false };
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
let observer: IntersectionObserver | null = null;
|
|
86
|
-
let motion: MediaQueryList | null = null;
|
|
87
|
-
|
|
88
|
-
async function start(): Promise<void> {
|
|
89
|
-
if (stop) return;
|
|
90
|
-
live.value = true;
|
|
91
|
-
await nextTick();
|
|
92
|
-
const el = canvas.value;
|
|
93
|
-
if (!el || !live.value) return;
|
|
94
|
-
const mark: Animated = { canvas: el, look, clock: 0, visible: true };
|
|
95
|
-
if (!drawNow(mark)) {
|
|
96
|
-
live.value = false;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
drawn.value = true;
|
|
100
|
-
stop = animate(mark);
|
|
101
|
-
if (typeof IntersectionObserver === "function") {
|
|
102
|
-
observer = new IntersectionObserver((entries) => {
|
|
103
|
-
for (const e of entries) mark.visible = e.isIntersecting;
|
|
104
|
-
});
|
|
105
|
-
observer.observe(el);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function halt(): void {
|
|
109
|
-
stop?.();
|
|
110
|
-
stop = null;
|
|
111
|
-
observer?.disconnect();
|
|
112
|
-
observer = null;
|
|
113
|
-
live.value = false;
|
|
114
|
-
drawn.value = false;
|
|
115
|
-
}
|
|
116
|
-
function onMotionChange(): void {
|
|
117
|
-
if (motion?.matches) halt();
|
|
118
|
-
else void start();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
onMounted(() => {
|
|
122
|
-
// No matchMedia means no way to honour reduced motion, and a test DOM
|
|
123
|
-
// (jsdom) has no canvas and logs an error for every attempt: keep the
|
|
124
|
-
// still SVG in both.
|
|
125
|
-
if (typeof window.matchMedia !== "function" || typeof requestAnimationFrame !== "function") return;
|
|
126
|
-
if (navigator.userAgent.includes("jsdom")) return;
|
|
127
|
-
motion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
128
|
-
motion.addEventListener?.("change", onMotionChange);
|
|
129
|
-
if (!motion.matches) void start();
|
|
130
|
-
});
|
|
131
|
-
onBeforeUnmount(() => {
|
|
132
|
-
motion?.removeEventListener?.("change", onMotionChange);
|
|
133
|
-
halt();
|
|
134
|
-
});
|
|
79
|
+
const { live, drawn } = useCanvasMotion(canvas, (ctx, width, ms) => drawPose(ctx, width, look(), poseAt(ms)));
|
|
135
80
|
</script>
|
|
136
81
|
|
|
137
82
|
<template>
|