@dumiflex/wp-kit 0.3.0 → 0.4.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 +77 -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 +119 -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 +292 -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 +125 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +173 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +175 -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 +82 -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/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/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/README.md
CHANGED
|
@@ -1,76 +1,73 @@
|
|
|
1
|
-
# wp-kit
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## License
|
|
75
|
-
|
|
76
|
-
[GPL-3.0-or-later](LICENSE). Copyright (C) 2026 DumiFlex.
|
|
1
|
+
# @dumiflex/wp-kit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@dumiflex/wp-kit)
|
|
4
|
+
[](https://vuejs.org/)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://github.com/DumiFlex/wp-kit/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
A Vue 3 component kit with its own theme: buttons, fields, selects, checkboxes, switches, chips, cards, tables, dialogs, drawers, popovers, menus, tooltips, tabs, sliders, colour, date and icon pickers, lists, toasts, code and diff views. The theme is design tokens, six colour palettes and seven looks (corners, type, borders). It was built for [Wildcard Pipeline](https://github.com/DumiFlex/ComfyUI-Wildcard-Pipeline), a ComfyUI extension, and works in any Vue 3 + Vite app.
|
|
9
|
+
|
|
10
|
+
Every piece reads CSS custom properties, so it takes on the colours, corners and type of the app it sits in.
|
|
11
|
+
|
|
12
|
+
**See every piece live, with when to use it:** [dumiflex.github.io/wp-kit](https://dumiflex.github.io/wp-kit/). The site has a switcher for the palette, the look and light or dark.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @dumiflex/wp-kit vue primeicons
|
|
18
|
+
# or: pnpm add @dumiflex/wp-kit vue primeicons
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The package ships source (`.vue`, `.ts` and CSS) with no build step, and your Vite app compiles the pieces it imports. In `vite.config`:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [vue()],
|
|
26
|
+
resolve: { dedupe: ["vue"] }, // one Vue, yours
|
|
27
|
+
optimizeDeps: { exclude: ["@dumiflex/wp-kit"] }, // .vue files go through your Vue plugin
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
Import one file at a time. There is no barrel file, so the bundler only ships what you use:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import "primeicons/primeicons.css";
|
|
37
|
+
import "@dumiflex/wp-kit/theme/tokens.css";
|
|
38
|
+
import "@dumiflex/wp-kit/theme/palettes.css";
|
|
39
|
+
import "@dumiflex/wp-kit/theme/looks.css";
|
|
40
|
+
import "@dumiflex/wp-kit/button.css";
|
|
41
|
+
|
|
42
|
+
import Button from "@dumiflex/wp-kit/Button.vue";
|
|
43
|
+
import Chip from "@dumiflex/wp-kit/Chip.vue";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```vue
|
|
47
|
+
<Button variant="primary" icon="pi-plus" @click="add">Add</Button>
|
|
48
|
+
<Chip tone="warn" icon="pi-exclamation-triangle">2 conflicts</Chip>
|
|
49
|
+
<Chip :pressed="tag === 'portrait'" :count="12" size="sm" @click="tag = 'portrait'">portrait</Chip>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Pick a palette and a look on `<html>`: `data-wp-palette="…"`, `data-wp-look="…"`, and the class `wp-theme-light` for light mode. The site lists them all and shows each piece's props and an example.
|
|
53
|
+
|
|
54
|
+
If you already import the kit as `@/kit/...`, alias `@/kit` to `node_modules/@dumiflex/wp-kit/src`.
|
|
55
|
+
|
|
56
|
+
## What your app provides
|
|
57
|
+
|
|
58
|
+
- **Vue 3.5** and **PrimeIcons 7** (peer dependencies). Icons are `pi pi-*` classes. The kit imports nothing else at runtime.
|
|
59
|
+
- **A theme:** the kit's own sheets above. A host with its own colour sheet can define the names in [`SHARED_TOKENS`](https://github.com/DumiFlex/wp-kit/blob/main/src/tokens.ts) instead and use the pieces that read only those.
|
|
60
|
+
|
|
61
|
+
## Versions
|
|
62
|
+
|
|
63
|
+
[Semantic versioning](https://semver.org/). Below 1.0, a minor version can break: read the [changelog](https://github.com/DumiFlex/wp-kit/blob/main/CHANGELOG.md) before you bump. Each release is published from CI with provenance.
|
|
64
|
+
|
|
65
|
+
## Links
|
|
66
|
+
|
|
67
|
+
- [Live components](https://dumiflex.github.io/wp-kit/)
|
|
68
|
+
- [Source and issues](https://github.com/DumiFlex/wp-kit)
|
|
69
|
+
- [Changelog](https://github.com/DumiFlex/wp-kit/blob/main/CHANGELOG.md)
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
[GPL-3.0-or-later](https://github.com/DumiFlex/wp-kit/blob/main/LICENSE). Copyright (C) 2026 DumiFlex.
|
package/package.json
CHANGED
|
@@ -1,77 +1,78 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dumiflex/wp-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Vue 3 component kit shared by the Wildcard Pipeline manager, canvas and community site.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"vue",
|
|
7
|
-
"vue3",
|
|
8
|
-
"components",
|
|
9
|
-
"design-tokens",
|
|
10
|
-
"comfyui",
|
|
11
|
-
"wildcard-pipeline"
|
|
12
|
-
],
|
|
13
|
-
"homepage": "https://dumiflex.github.io/wp-kit/",
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/DumiFlex/wp-kit/issues"
|
|
16
|
-
},
|
|
17
|
-
"license": "GPL-3.0-or-later",
|
|
18
|
-
"type": "module",
|
|
19
|
-
"files": [
|
|
20
|
-
"src",
|
|
21
|
-
"!src/**/__tests__",
|
|
22
|
-
"!src/**/*.test.ts",
|
|
23
|
-
"CHANGELOG.md"
|
|
24
|
-
],
|
|
25
|
-
"exports": {
|
|
26
|
-
"./package.json": "./package.json",
|
|
27
|
-
"./*": "./src/*"
|
|
28
|
-
},
|
|
29
|
-
"sideEffects": [
|
|
30
|
-
"**/*.css",
|
|
31
|
-
"**/*.vue"
|
|
32
|
-
],
|
|
33
|
-
"publishConfig": {
|
|
34
|
-
"access": "public"
|
|
35
|
-
},
|
|
36
|
-
"repository": {
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/DumiFlex/wp-kit.git"
|
|
39
|
-
},
|
|
40
|
-
"packageManager": "pnpm@9.12.0",
|
|
41
|
-
"engines": {
|
|
42
|
-
"node": ">=22"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"lint": "eslint .",
|
|
46
|
-
"typecheck": "vue-tsc --noEmit",
|
|
47
|
-
"test": "vitest run",
|
|
48
|
-
"test:watch": "vitest",
|
|
49
|
-
"site:dev": "vite --config site/vite.config.ts",
|
|
50
|
-
"site:build": "vite build --config site/vite.config.ts",
|
|
51
|
-
"site:preview": "vite preview --config site/vite.config.ts",
|
|
52
|
-
"check-package": "node scripts/check-package.mjs"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"@
|
|
61
|
-
"@typescript-eslint/
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
64
|
-
"@vue/
|
|
65
|
-
"
|
|
66
|
-
"eslint
|
|
67
|
-
"eslint-plugin-
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"vue
|
|
75
|
-
"vue-
|
|
76
|
-
|
|
77
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dumiflex/wp-kit",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Vue 3 component kit shared by the Wildcard Pipeline manager, canvas and community site.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vue",
|
|
7
|
+
"vue3",
|
|
8
|
+
"components",
|
|
9
|
+
"design-tokens",
|
|
10
|
+
"comfyui",
|
|
11
|
+
"wildcard-pipeline"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://dumiflex.github.io/wp-kit/",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/DumiFlex/wp-kit/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "GPL-3.0-or-later",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"!src/**/__tests__",
|
|
22
|
+
"!src/**/*.test.ts",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./*": "./src/*"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": [
|
|
30
|
+
"**/*.css",
|
|
31
|
+
"**/*.vue"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/DumiFlex/wp-kit.git"
|
|
39
|
+
},
|
|
40
|
+
"packageManager": "pnpm@9.12.0",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"typecheck": "vue-tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"site:dev": "vite --config site/vite.config.ts",
|
|
50
|
+
"site:build": "vite build --config site/vite.config.ts",
|
|
51
|
+
"site:preview": "vite preview --config site/vite.config.ts",
|
|
52
|
+
"check-package": "node scripts/npm-readme.mjs --check && node scripts/check-package.mjs",
|
|
53
|
+
"npm-readme": "node scripts/npm-readme.mjs"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"vue": "^3.5.0",
|
|
57
|
+
"primeicons": "^7.0.0 || ^8.0.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.10.0",
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "8.59.0",
|
|
62
|
+
"@typescript-eslint/parser": "8.59.0",
|
|
63
|
+
"@vitejs/plugin-vue": "5.2.4",
|
|
64
|
+
"@vue/test-utils": "2.4.8",
|
|
65
|
+
"@vue/tsconfig": "0.5.1",
|
|
66
|
+
"eslint": "9.39.4",
|
|
67
|
+
"eslint-plugin-vue": "9.33.0",
|
|
68
|
+
"eslint-plugin-vuejs-accessibility": "^2.5.0",
|
|
69
|
+
"jsdom": "29.0.2",
|
|
70
|
+
"primeicons": "7.0.0",
|
|
71
|
+
"typescript": "5.9.3",
|
|
72
|
+
"vite": "5.4.21",
|
|
73
|
+
"vitest": "2.1.9",
|
|
74
|
+
"vue": "3.5.44",
|
|
75
|
+
"vue-eslint-parser": "10.4.0",
|
|
76
|
+
"vue-tsc": "2.2.12"
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/Avatar.vue
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Avatar: a person's picture, or their initials on a colour picked from
|
|
4
|
-
* their name when there is no picture (or it fails to load). For community
|
|
5
|
-
* authors, chat participants and history entries. Works on the canvas and
|
|
6
|
-
* in the manager.
|
|
7
|
-
*
|
|
8
|
-
* <Avatar name="Dumi Flex" :src="author.avatar_url" />
|
|
9
|
-
* <Avatar name="Pack bot" icon="pi-box" size="sm" />
|
|
10
|
-
*
|
|
11
|
-
* The name is the image's alt text; with `decorative` (the name is already
|
|
12
|
-
* written beside it) screen readers skip it.
|
|
13
|
-
*/
|
|
14
|
-
import { computed, ref, watch } from "vue";
|
|
15
|
-
|
|
16
|
-
const props = withDefaults(defineProps<{
|
|
17
|
-
name: string;
|
|
18
|
-
src?: string | null | undefined;
|
|
19
|
-
/** A PrimeIcons class to draw instead of initials. */
|
|
20
|
-
icon?: string | undefined;
|
|
21
|
-
size?: "sm" | "md" | "lg" | undefined;
|
|
22
|
-
shape?: "circle" | "square" | undefined;
|
|
23
|
-
/** The name is written next to it already. */
|
|
24
|
-
decorative?: boolean | undefined;
|
|
25
|
-
}>(), { src: null, icon: undefined, size: "md", shape: "circle" });
|
|
26
|
-
|
|
27
|
-
const failed = ref(false);
|
|
28
|
-
watch(() => props.src, () => { failed.value = false; });
|
|
29
|
-
|
|
30
|
-
const initials = computed(() => {
|
|
31
|
-
const words = props.name.trim().split(/\s+/).filter(Boolean);
|
|
32
|
-
if (!words.length) return "?";
|
|
33
|
-
const first = [...words[0]][0] ?? "";
|
|
34
|
-
const last = words.length > 1 ? [...words[words.length - 1]][0] ?? "" : "";
|
|
35
|
-
return (first + last).toUpperCase();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const HUES = [
|
|
39
|
-
"var(--wp-var-1)", "var(--wp-var-2)", "var(--wp-var-3)", "var(--wp-var-4)",
|
|
40
|
-
"var(--wp-var-5)", "var(--wp-var-6)", "var(--wp-var-7)", "var(--wp-var-8)",
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
/** One of the eight variable colours, the same for the same name every time. */
|
|
44
|
-
const hue = computed(() => {
|
|
45
|
-
let h = 0;
|
|
46
|
-
for (const ch of props.name) h = (h * 31 + (ch.codePointAt(0) ?? 0)) >>> 0;
|
|
47
|
-
return HUES[h % 8];
|
|
48
|
-
});
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<template>
|
|
52
|
-
<span
|
|
53
|
-
class="wp-avatar"
|
|
54
|
-
:data-size="size"
|
|
55
|
-
:data-shape="shape"
|
|
56
|
-
:style="{ '--_hue': hue }"
|
|
57
|
-
:role="decorative ? undefined : 'img'"
|
|
58
|
-
:aria-label="decorative ? undefined : name"
|
|
59
|
-
:aria-hidden="decorative ? 'true' : undefined"
|
|
60
|
-
>
|
|
61
|
-
<img v-if="src && !failed" class="wp-avatar__img" :src="src" alt="" @error="failed = true">
|
|
62
|
-
<i v-else-if="icon" class="pi wp-avatar__icon" :class="icon" aria-hidden="true" />
|
|
63
|
-
<span v-else class="wp-avatar__initials" aria-hidden="true">{{ initials }}</span>
|
|
64
|
-
</span>
|
|
65
|
-
</template>
|
|
66
|
-
|
|
67
|
-
<style scoped>
|
|
68
|
-
.wp-avatar {
|
|
69
|
-
--_s: var(--wp-control-h-md);
|
|
70
|
-
position: relative; display: inline-grid; place-items: center; flex: none; overflow: hidden;
|
|
71
|
-
width: var(--_s); height: var(--_s); border-radius: 50%; /* round: avatar */
|
|
72
|
-
background: color-mix(in srgb, var(--_hue) 22%, var(--wp-bg-2));
|
|
73
|
-
color: color-mix(in srgb, var(--_hue) 70%, var(--wp-text));
|
|
74
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_hue) 30%, transparent);
|
|
75
|
-
font: 600 calc(var(--_s) * 0.4)/1 var(--wp-font-sans); letter-spacing: 0.02em; user-select: none;
|
|
76
|
-
}
|
|
77
|
-
.wp-avatar[data-size="sm"] { --_s: var(--wp-control-h-sm); }
|
|
78
|
-
.wp-avatar[data-size="lg"] { --_s: var(--wp-control-h-lg); }
|
|
79
|
-
.wp-avatar[data-shape="square"] { border-radius: var(--wp-radius); }
|
|
80
|
-
.wp-avatar[data-size="sm"][data-shape="square"] { border-radius: var(--wp-radius-sm); }
|
|
81
|
-
.wp-avatar__img { width: 100%; height: 100%; object-fit: cover; }
|
|
82
|
-
.wp-avatar__icon { font-size: calc(var(--_s) * 0.45); }
|
|
83
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Avatar: a person's picture, or their initials on a colour picked from
|
|
4
|
+
* their name when there is no picture (or it fails to load). For community
|
|
5
|
+
* authors, chat participants and history entries. Works on the canvas and
|
|
6
|
+
* in the manager.
|
|
7
|
+
*
|
|
8
|
+
* <Avatar name="Dumi Flex" :src="author.avatar_url" />
|
|
9
|
+
* <Avatar name="Pack bot" icon="pi-box" size="sm" />
|
|
10
|
+
*
|
|
11
|
+
* The name is the image's alt text; with `decorative` (the name is already
|
|
12
|
+
* written beside it) screen readers skip it.
|
|
13
|
+
*/
|
|
14
|
+
import { computed, ref, watch } from "vue";
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
name: string;
|
|
18
|
+
src?: string | null | undefined;
|
|
19
|
+
/** A PrimeIcons class to draw instead of initials. */
|
|
20
|
+
icon?: string | undefined;
|
|
21
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
22
|
+
shape?: "circle" | "square" | undefined;
|
|
23
|
+
/** The name is written next to it already. */
|
|
24
|
+
decorative?: boolean | undefined;
|
|
25
|
+
}>(), { src: null, icon: undefined, size: "md", shape: "circle" });
|
|
26
|
+
|
|
27
|
+
const failed = ref(false);
|
|
28
|
+
watch(() => props.src, () => { failed.value = false; });
|
|
29
|
+
|
|
30
|
+
const initials = computed(() => {
|
|
31
|
+
const words = props.name.trim().split(/\s+/).filter(Boolean);
|
|
32
|
+
if (!words.length) return "?";
|
|
33
|
+
const first = [...words[0]][0] ?? "";
|
|
34
|
+
const last = words.length > 1 ? [...words[words.length - 1]][0] ?? "" : "";
|
|
35
|
+
return (first + last).toUpperCase();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const HUES = [
|
|
39
|
+
"var(--wp-var-1)", "var(--wp-var-2)", "var(--wp-var-3)", "var(--wp-var-4)",
|
|
40
|
+
"var(--wp-var-5)", "var(--wp-var-6)", "var(--wp-var-7)", "var(--wp-var-8)",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** One of the eight variable colours, the same for the same name every time. */
|
|
44
|
+
const hue = computed(() => {
|
|
45
|
+
let h = 0;
|
|
46
|
+
for (const ch of props.name) h = (h * 31 + (ch.codePointAt(0) ?? 0)) >>> 0;
|
|
47
|
+
return HUES[h % 8];
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<span
|
|
53
|
+
class="wp-avatar"
|
|
54
|
+
:data-size="size"
|
|
55
|
+
:data-shape="shape"
|
|
56
|
+
:style="{ '--_hue': hue }"
|
|
57
|
+
:role="decorative ? undefined : 'img'"
|
|
58
|
+
:aria-label="decorative ? undefined : name"
|
|
59
|
+
:aria-hidden="decorative ? 'true' : undefined"
|
|
60
|
+
>
|
|
61
|
+
<img v-if="src && !failed" class="wp-avatar__img" :src="src" alt="" @error="failed = true">
|
|
62
|
+
<i v-else-if="icon" class="pi wp-avatar__icon" :class="icon" aria-hidden="true" />
|
|
63
|
+
<span v-else class="wp-avatar__initials" aria-hidden="true">{{ initials }}</span>
|
|
64
|
+
</span>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style scoped>
|
|
68
|
+
.wp-avatar {
|
|
69
|
+
--_s: var(--wp-control-h-md);
|
|
70
|
+
position: relative; display: inline-grid; place-items: center; flex: none; overflow: hidden;
|
|
71
|
+
width: var(--_s); height: var(--_s); border-radius: 50%; /* round: avatar */
|
|
72
|
+
background: color-mix(in srgb, var(--_hue) 22%, var(--wp-bg-2));
|
|
73
|
+
color: color-mix(in srgb, var(--_hue) 70%, var(--wp-text));
|
|
74
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_hue) 30%, transparent);
|
|
75
|
+
font: 600 calc(var(--_s) * 0.4)/1 var(--wp-font-sans); letter-spacing: 0.02em; user-select: none;
|
|
76
|
+
}
|
|
77
|
+
.wp-avatar[data-size="sm"] { --_s: var(--wp-control-h-sm); }
|
|
78
|
+
.wp-avatar[data-size="lg"] { --_s: var(--wp-control-h-lg); }
|
|
79
|
+
.wp-avatar[data-shape="square"] { border-radius: var(--wp-radius); }
|
|
80
|
+
.wp-avatar[data-size="sm"][data-shape="square"] { border-radius: var(--wp-radius-sm); }
|
|
81
|
+
.wp-avatar__img { width: 100%; height: 100%; object-fit: cover; }
|
|
82
|
+
.wp-avatar__icon { font-size: calc(var(--_s) * 0.45); }
|
|
83
|
+
</style>
|