mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
412 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c0e0ec823 | ||
|
|
4f56921096 | ||
|
|
6a5ee32e05 | ||
|
|
4ea07e1077 | ||
|
|
5fd65d0917 | ||
|
|
2ec0cee1d9 | ||
|
|
758e6f1400 | ||
|
|
275fa1831d | ||
|
|
8b5e08f6f2 | ||
|
|
1635f57de6 | ||
|
|
b3e0122001 | ||
|
|
4f9d20e603 | ||
|
|
f7add47cf2 | ||
|
|
99c1c683eb | ||
|
|
ff9f6c251d | ||
|
|
e0c703ca6c | ||
|
|
2210faa160 | ||
|
|
19589b5e05 | ||
|
|
7051fa39a7 | ||
|
|
911278e95e | ||
|
|
c015148f29 | ||
|
|
cb1fd55801 | ||
|
|
16fd1c0ca3 | ||
|
|
84ac92ed7a | ||
|
|
0ade69de26 | ||
|
|
d9193abf55 | ||
|
|
44a78d7f67 | ||
|
|
3ce600f89a | ||
|
|
662dd9ee65 | ||
|
|
3be3751bfc | ||
|
|
5042b5806b | ||
|
|
2535301bcd | ||
|
|
629bb72424 | ||
|
|
543b4e025f | ||
|
|
7f18c6bdc7 | ||
|
|
0ea924ca0d | ||
|
|
22f3d42470 | ||
|
|
1ad96065fd | ||
|
|
952786ed79 | ||
|
|
9cf92e34ab | ||
|
|
6321d3ed8d | ||
|
|
bc0c168c0b | ||
|
|
9975305f2a | ||
|
|
edc1bd677b | ||
|
|
32d1f21299 | ||
|
|
f901e417e1 | ||
|
|
23921e1ae2 | ||
|
|
8a5f52dbc8 | ||
|
|
04d5ce1c51 | ||
|
|
3a300f72c1 | ||
|
|
927debfb6b | ||
|
|
44176ee897 | ||
|
|
7f9d69183c | ||
|
|
23e5f4c501 | ||
|
|
24998e3ac5 | ||
|
|
bebf18a89a | ||
|
|
1b56b50d4d | ||
|
|
3337559b89 | ||
|
|
d33a23ebc0 | ||
|
|
28586c35a5 | ||
|
|
e521e1ac24 | ||
|
|
8ea3223071 | ||
|
|
4cbe983f61 | ||
|
|
e42969f003 | ||
|
|
c93e37a0eb | ||
|
|
9e20f96b65 | ||
|
|
9f6f132a76 | ||
|
|
5517cc2846 | ||
|
|
8c24b5dc97 | ||
|
|
ec8bd5cdc4 | ||
|
|
6fab68baa8 | ||
|
|
fc951e2980 | ||
|
|
08ff6e6c09 | ||
|
|
9dcdaf474e | ||
|
|
e9f0224b91 | ||
|
|
929192fd46 | ||
|
|
d9ff11d1a2 | ||
|
|
80edb1bd4b | ||
|
|
fc658842bb | ||
|
|
32922def7d | ||
|
|
a4bea1c508 | ||
|
|
f87252f05d | ||
|
|
ec9f67094a | ||
|
|
72dc0d0d0c | ||
|
|
02b0a29d86 | ||
|
|
cd2f1122be | ||
|
|
e3943123ce | ||
|
|
d57647a77a | ||
|
|
aa97f26180 | ||
|
|
0b17fb02e4 | ||
|
|
5039265097 | ||
|
|
c89c65bd44 | ||
|
|
4515a1239b | ||
|
|
de9596bfc4 | ||
|
|
7da11ccec0 | ||
|
|
d29377f614 | ||
|
|
f0e482cf01 | ||
|
|
056ab30474 | ||
|
|
95c14a4360 | ||
|
|
b25bb75783 | ||
|
|
98ae6c14aa | ||
|
|
8ee2a3cf5b | ||
|
|
91f93c450e | ||
|
|
237a673af3 | ||
|
|
591e60dfc7 | ||
|
|
b9f0b3cb10 | ||
|
|
3ed64250cd | ||
|
|
f1b59fc59e | ||
|
|
005c18e4c0 | ||
|
|
44b7199c4f | ||
|
|
4774adc0ae | ||
|
|
03b0e07a53 | ||
|
|
2560088b2b | ||
|
|
4ce991652b | ||
|
|
d96b4ea119 | ||
|
|
b20c8c82ab | ||
|
|
e5bf052c1f | ||
|
|
cf1b2cdd13 | ||
|
|
8776929f63 | ||
|
|
62361bfa8f | ||
|
|
bf25ad15af | ||
|
|
03b18b1ff8 | ||
|
|
fdf149f0d0 | ||
|
|
e4c3a74e0e | ||
|
|
38f248c24d | ||
|
|
74de5b106b | ||
|
|
70f8cd4889 | ||
|
|
6a2159da94 | ||
|
|
ae6b3c0ba6 | ||
|
|
70495e0a22 | ||
|
|
be99df3b30 | ||
|
|
6870eff661 | ||
|
|
062e468518 | ||
|
|
3fc2a043fa | ||
|
|
e99ec9b82b | ||
|
|
4556f24eb1 | ||
|
|
f8fb66f177 | ||
|
|
5d760d9d8d | ||
|
|
cb5c9b3923 | ||
|
|
8e9418f94f | ||
|
|
4712726fbd | ||
|
|
0cb38e2214 | ||
|
|
3e9c502fbe | ||
|
|
541ed304a0 | ||
|
|
72919425b6 | ||
|
|
8a66f5e9bf | ||
|
|
a0d56d0f66 | ||
|
|
c6706c76b2 | ||
|
|
1f7f28835d | ||
|
|
593573a286 | ||
|
|
3a6ecd6988 | ||
|
|
f9dbe28938 | ||
|
|
2ae17ed919 | ||
|
|
abb93b5fd3 | ||
|
|
f75fc4f864 | ||
|
|
5722a3ae62 | ||
|
|
aa242aa87d | ||
|
|
945fec62c2 | ||
|
|
9302b8d635 | ||
|
|
b43394ddc3 | ||
|
|
be94fea84a | ||
|
|
c08501db3f | ||
|
|
db2b73e741 | ||
|
|
6b813bd3b3 | ||
|
|
7b3263a621 | ||
|
|
002a8f6803 | ||
|
|
5a596eb2b0 | ||
|
|
2f87cf86a1 | ||
|
|
e4f148efa9 | ||
|
|
024a5914b9 | ||
|
|
ce28b04187 | ||
|
|
1ff9fd4f69 | ||
|
|
487f253e14 | ||
|
|
f54d4d788d | ||
|
|
c04885dd79 | ||
|
|
8d70dbdcfe | ||
|
|
597439b538 | ||
|
|
dc1050225b | ||
|
|
b44e3af422 | ||
|
|
d9f08b99ca | ||
|
|
af870f90ec | ||
|
|
222bb987a4 | ||
|
|
ea293bae0c | ||
|
|
8a57ab60c7 | ||
|
|
9e0edc27ab | ||
|
|
76ffbf4cf3 | ||
|
|
4eb4b65167 | ||
|
|
cf65b4ab54 | ||
|
|
c4e2197584 | ||
|
|
1b5f7f5e31 | ||
|
|
1495ff987d | ||
|
|
503b9a6b5c | ||
|
|
5e4c49ae3a | ||
|
|
c42dc9ecd2 | ||
|
|
18dceb7445 | ||
|
|
1a8ca6fb38 | ||
|
|
2d7f096a55 | ||
|
|
da4e8d5c09 | ||
|
|
5fdf5eede0 | ||
|
|
d413cf74d6 | ||
|
|
5906ee9c64 | ||
|
|
0e0f3e39d3 | ||
|
|
7907b1274d | ||
|
|
46ea467098 | ||
|
|
1d8958c2fc | ||
|
|
9a52db84b7 | ||
|
|
78dad77a44 | ||
|
|
144b3382ad | ||
|
|
b00a3aa7d6 | ||
|
|
498be15a29 | ||
|
|
95c018f63f | ||
|
|
efe9ab7140 | ||
|
|
2dd9771d0d | ||
|
|
4de20563c3 | ||
|
|
40fef2bdf5 | ||
|
|
00b9a0839b | ||
|
|
0e1b9087af | ||
|
|
5b4e4f8648 | ||
|
|
6e616408ea | ||
|
|
b1bbbbb7d0 | ||
|
|
db39a9cdba | ||
|
|
524f8411c5 | ||
|
|
22ee717105 | ||
|
|
eb6fbd9c4a | ||
|
|
ae6e8eec03 | ||
|
|
2e64343610 | ||
|
|
6da0c28019 | ||
|
|
8f9e0a4a50 | ||
|
|
06426ad1e5 | ||
|
|
7db0ca50a0 | ||
|
|
10d89d3cd1 | ||
|
|
00e0ab39f8 | ||
|
|
5a8b078d65 | ||
|
|
f98253c255 | ||
|
|
7f7b158b7e | ||
|
|
bbbe57dedb | ||
|
|
a689542de5 | ||
|
|
847788b2a0 | ||
|
|
d733e25bf0 | ||
|
|
f0835cf979 | ||
|
|
33bc561759 | ||
|
|
8039351bee | ||
|
|
7979bc6586 | ||
|
|
2f90ce2319 | ||
|
|
aecfef20e6 | ||
|
|
c3a200d450 | ||
|
|
dd6463710c | ||
|
|
33b1176bdd | ||
|
|
ce899b4e27 | ||
|
|
536bfe4ff1 | ||
|
|
6585bfc24a | ||
|
|
b05aa48caa | ||
|
|
e419d68f64 | ||
|
|
06b07e292e | ||
|
|
33a31205a1 | ||
|
|
4f7e286790 | ||
|
|
c1a7629e29 | ||
|
|
189c9f3eda | ||
|
|
358387e358 | ||
|
|
3c724e89d4 | ||
|
|
4851a4b905 | ||
|
|
4f789ac54b | ||
|
|
72756689b9 | ||
|
|
d980176e03 | ||
|
|
23deef325a | ||
|
|
4b4d16503f | ||
|
|
0154fa6e8c | ||
|
|
39bf242f78 | ||
|
|
6c2f93f262 | ||
|
|
5031610d05 | ||
|
|
b9757384a5 | ||
|
|
60c72a20d1 | ||
|
|
879bf36698 | ||
|
|
f65f4aa19e | ||
|
|
9651f4a216 | ||
|
|
dd292afa5e | ||
|
|
538f707757 | ||
|
|
051d089928 | ||
|
|
8045c7b47e | ||
|
|
0e18526a6f | ||
|
|
cc561f971b | ||
|
|
c4a40b065c | ||
|
|
8222d05c15 | ||
|
|
7d1c38c820 | ||
|
|
dfe86f0baf | ||
|
|
6532b4bfdd | ||
|
|
936d6a5fee | ||
|
|
cde6b5037d | ||
|
|
5de5c07203 | ||
|
|
0f25820a49 | ||
|
|
f48dec0d60 | ||
|
|
f0bfe20572 | ||
|
|
1bda2ec01f | ||
|
|
57354f64f9 | ||
|
|
035919a545 | ||
|
|
18c194e839 | ||
|
|
15b4f913ae | ||
|
|
723f075c56 | ||
|
|
39790058af | ||
|
|
2e099392f1 | ||
|
|
8bc4902078 | ||
|
|
656b6e1c59 | ||
|
|
2eb2feab67 | ||
|
|
fc4de89dad | ||
|
|
c431f8b4a1 | ||
|
|
289de07ba1 | ||
|
|
368f93c294 | ||
|
|
49afd2d404 | ||
|
|
77149f0dd4 | ||
|
|
3ff8ae70b1 | ||
|
|
0d51747f4a | ||
|
|
5c45ac9882 | ||
|
|
ba643d9faa | ||
|
|
571e907acd | ||
|
|
065d3691ac | ||
|
|
564ae86d63 | ||
|
|
cf021a5888 | ||
|
|
7237dcb10f | ||
|
|
0ca40a57c9 | ||
|
|
3c7d986684 | ||
|
|
c59b45cf3c | ||
|
|
685f738d5c | ||
|
|
eb0043914b | ||
|
|
cc01af8e55 | ||
|
|
eb41b23432 | ||
|
|
f38b920305 | ||
|
|
dca0ff4a60 | ||
|
|
fb3ff2e5fa | ||
|
|
9554e801c2 | ||
|
|
cdb4b1f233 | ||
|
|
3305f28c22 | ||
|
|
e8a1b46598 | ||
|
|
c71fc69b4b | ||
|
|
fc2d17cbc3 | ||
|
|
850c766fab | ||
|
|
da3ed26c2c | ||
|
|
4d0709a25c | ||
|
|
7723704f79 | ||
|
|
77372423d8 | ||
|
|
d4b65996ce | ||
|
|
55565d7079 | ||
|
|
858b7da3c0 | ||
|
|
75440608d0 | ||
|
|
1e6ad72644 | ||
|
|
6cfdebb564 | ||
|
|
7e85c07d72 | ||
|
|
a584d62119 | ||
|
|
ceedbe0bbd | ||
|
|
aef11562c9 | ||
|
|
09aed4b752 | ||
|
|
0d98d44045 | ||
|
|
c3facb1fef | ||
|
|
78021d3850 | ||
|
|
3d9ead3d8d | ||
|
|
5fb7f10283 | ||
|
|
95b4b10c8e | ||
|
|
b3e288aabc | ||
|
|
ba15aed671 | ||
|
|
7980e7f691 | ||
|
|
46ca8c5422 | ||
|
|
fc50834448 | ||
|
|
f1a830f7a6 | ||
|
|
3ab0698d7f | ||
|
|
6fc9466e8e | ||
|
|
6484575490 | ||
|
|
9406124ee1 | ||
|
|
4532e09ac0 | ||
|
|
e1d79d7fe7 | ||
|
|
516e7faf8f | ||
|
|
c0661c3580 | ||
|
|
223a477a5e | ||
|
|
8492e161db | ||
|
|
4e0d23ed34 | ||
|
|
13193969ee | ||
|
|
a01286f427 | ||
|
|
f67fdb7d2f | ||
|
|
541bcf6d76 | ||
|
|
90ff1c0671 | ||
|
|
4c891225b1 | ||
|
|
c636ff24ba | ||
|
|
93c9fe1c74 | ||
|
|
4a648850f5 | ||
|
|
382e3b8e52 | ||
|
|
f513ea6ca8 | ||
|
|
b2705feedc | ||
|
|
b8dd80d3d3 | ||
|
|
65aa1690ba | ||
|
|
619f620b7e | ||
|
|
5b050ee52d | ||
|
|
702abf7a9f | ||
|
|
1bec8d163c | ||
|
|
a9f1d937bc | ||
|
|
185273f3e9 | ||
|
|
757c38e75c | ||
|
|
6bd51975bf | ||
|
|
b7f6bf93d3 | ||
|
|
b5edfaddd4 | ||
|
|
45cf898ec3 | ||
|
|
57d477f3a2 | ||
|
|
d745d1aa9a | ||
|
|
5288dce744 | ||
|
|
760da3d1a8 | ||
|
|
832ffe4323 | ||
|
|
ed499b3b21 | ||
|
|
5e44c54ae5 | ||
|
|
d724f87654 | ||
|
|
af566ab1fa | ||
|
|
85c8210ede | ||
|
|
07df347fe3 | ||
|
|
e0342184fe | ||
|
|
e52d22de34 | ||
|
|
a564f79b0c |
3
.github/workflows/ci-dev.yml
vendored
3
.github/workflows/ci-dev.yml
vendored
@@ -41,6 +41,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -41,6 +41,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
|
||||
260
CHANGELOG.md
260
CHANGELOG.md
@@ -2,6 +2,266 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.1.26](https://github.com/nuxtlabs/ui/compare/v0.1.25...v0.1.26) (2023-01-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **CommandPalette:** select first item on search changes ([#126](https://github.com/nuxtlabs/ui/issues/126)) ([4f56921](https://github.com/nuxtlabs/ui/commit/4f56921096f5885cdab8b7cb5c5aa01304188e11))
|
||||
|
||||
### [0.1.25](https://github.com/nuxtlabs/ui/compare/v0.1.24...v0.1.25) (2023-01-09)
|
||||
|
||||
### [0.1.24](https://github.com/nuxtlabs/ui/compare/v0.1.23...v0.1.24) (2023-01-04)
|
||||
|
||||
### [0.1.23](https://github.com/nuxtlabs/ui/compare/v0.1.22...v0.1.23) (2022-12-20)
|
||||
|
||||
### [0.1.22](https://github.com/nuxtlabs/ui/compare/v0.1.21...v0.1.22) (2022-12-19)
|
||||
|
||||
### [0.1.21](https://github.com/nuxtlabs/ui/compare/v0.1.20...v0.1.21) (2022-12-19)
|
||||
|
||||
### [0.1.20](https://github.com/nuxtlabs/ui/compare/v0.1.19...v0.1.20) (2022-12-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid referring to complex types in props ([#123](https://github.com/nuxtlabs/ui/issues/123)) ([ff9f6c2](https://github.com/nuxtlabs/ui/commit/ff9f6c251df59641862d82587e5d963c8e6ea298))
|
||||
|
||||
### [0.1.19](https://github.com/nuxtlabs/ui/compare/v0.1.18...v0.1.19) (2022-12-16)
|
||||
|
||||
### [0.1.18](https://github.com/nuxtlabs/ui/compare/v0.1.17...v0.1.18) (2022-12-15)
|
||||
|
||||
### [0.1.17](https://github.com/nuxtlabs/ui/compare/v0.1.16...v0.1.17) (2022-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove stop propagation on mode hover ([16fd1c0](https://github.com/nuxtlabs/ui/commit/16fd1c0ca38f1438e791c0d44399f590d9f20d02))
|
||||
|
||||
### [0.1.16](https://github.com/nuxtlabs/ui/compare/v0.1.15...v0.1.16) (2022-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Popover:** preset from tooltip ([0ade69d](https://github.com/nuxtlabs/ui/commit/0ade69de2689b094b11a2dead8f71e3d2dccd552))
|
||||
|
||||
### [0.1.15](https://github.com/nuxtlabs/ui/compare/v0.1.14...v0.1.15) (2022-12-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Dropdown:** better handle item click to preventDefault ([#119](https://github.com/nuxtlabs/ui/issues/119)) ([44a78d7](https://github.com/nuxtlabs/ui/commit/44a78d7f679812c59d4410d0bbc01f09abaa78dd))
|
||||
|
||||
### [0.1.14](https://github.com/nuxtlabs/ui/compare/v0.1.13...v0.1.14) (2022-12-02)
|
||||
|
||||
### [0.1.13](https://github.com/nuxtlabs/ui/compare/v0.1.12...v0.1.13) (2022-12-01)
|
||||
|
||||
### [0.1.12](https://github.com/nuxtlabs/ui/compare/v0.1.11...v0.1.12) (2022-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Checkbox:** types ([629bb72](https://github.com/nuxtlabs/ui/commit/629bb724249cfe1fdd999cf52f8e72ca444bd94d))
|
||||
|
||||
### [0.1.11](https://github.com/nuxtlabs/ui/compare/v0.1.10...v0.1.11) (2022-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Checkbox:** revert type fix as it breaks checkboxes ([7f18c6b](https://github.com/nuxtlabs/ui/commit/7f18c6bdc7c0054b2e5d4f9bf4e362847a3ba3a3))
|
||||
|
||||
### [0.1.10](https://github.com/nuxtlabs/ui/compare/v0.1.9...v0.1.10) (2022-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default popper options ([1ad9606](https://github.com/nuxtlabs/ui/commit/1ad96065fd706d828b906db3a5d578226ff08c36))
|
||||
* default props for command palette ([#116](https://github.com/nuxtlabs/ui/issues/116)) ([952786e](https://github.com/nuxtlabs/ui/commit/952786ed79cd9cdf523f6eac5958f68790bacbea))
|
||||
|
||||
### [0.1.9](https://github.com/nuxtlabs/ui/compare/v0.1.8...v0.1.9) (2022-11-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Icon:** couldn't load anymore ([6321d3e](https://github.com/nuxtlabs/ui/commit/6321d3ed8d5c9478cb1dafc1da94b21d0c7edb88))
|
||||
* **Icon:** eslint ignore ([bc0c168](https://github.com/nuxtlabs/ui/commit/bc0c168c0b0feae96d6a1848c3a356d846e2cbb5))
|
||||
|
||||
### [0.1.8](https://github.com/nuxtlabs/ui/compare/v0.1.7...v0.1.8) (2022-11-16)
|
||||
|
||||
### [0.1.7](https://github.com/nuxtlabs/ui/compare/v0.1.6...v0.1.7) (2022-11-16)
|
||||
|
||||
### [0.1.6](https://github.com/nuxtlabs/ui/compare/v0.1.5...v0.1.6) (2022-11-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SelectCustom:** add `w-full` on `ComboboxButton` ([3a300f7](https://github.com/nuxtlabs/ui/commit/3a300f72c1eca756ffd8f07ab871bf9c7bd7868d))
|
||||
|
||||
### [0.1.5](https://github.com/nuxtlabs/ui/compare/v0.1.4...v0.1.5) (2022-11-08)
|
||||
|
||||
### [0.1.4](https://github.com/nuxtlabs/ui/compare/v0.1.3...v0.1.4) (2022-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **button:** support `RouteLocationRaw` type for `to` ([#112](https://github.com/nuxtlabs/ui/issues/112)) ([1b56b50](https://github.com/nuxtlabs/ui/commit/1b56b50d4d54a5cb9e5febacdf42867988ae6c5d))
|
||||
|
||||
### [0.1.3](https://github.com/nuxtlabs/ui/compare/v0.1.2...v0.1.3) (2022-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docs:** component input string field ([e521e1a](https://github.com/nuxtlabs/ui/commit/e521e1ac2421dc331652f1ea4ac2b0b2959dc069))
|
||||
* **types:** add missing field in command palette type ([#111](https://github.com/nuxtlabs/ui/issues/111)) ([28586c3](https://github.com/nuxtlabs/ui/commit/28586c35a558d9e925094f86e07acdb928d54ad7))
|
||||
|
||||
### [0.1.2](https://github.com/nuxtlabs/ui/compare/v0.1.1...v0.1.2) (2022-10-27)
|
||||
|
||||
### [0.1.1](https://github.com/nuxtlabs/ui/compare/v0.1.0...v0.1.1) (2022-10-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **CommandPalette:** lint ([6fab68b](https://github.com/nuxtlabs/ui/commit/6fab68baa836c97680446e8cfdee7c5a64008539))
|
||||
* **Dropdown:** close on click item with `to` ([#103](https://github.com/nuxtlabs/ui/issues/103)) ([5517cc2](https://github.com/nuxtlabs/ui/commit/5517cc28467f957956a20126c1ce48e64677cb82))
|
||||
* **Popover:** avoid crash on mount if ref not loaded ([#105](https://github.com/nuxtlabs/ui/issues/105)) ([e9f0224](https://github.com/nuxtlabs/ui/commit/e9f0224b919445260d3b19511420a3fd448e4ea7))
|
||||
* **SelectCustom:** types and lint ([ec8bd5c](https://github.com/nuxtlabs/ui/commit/ec8bd5cdc49feb924dfdff352d9f3d788653c583))
|
||||
|
||||
## [0.1.0](https://github.com/nuxtlabs/ui/compare/v0.0.3...v0.1.0) (2022-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `to` prop type ([be94fea](https://github.com/nuxtlabs/ui/commit/be94fea84acc69c0114099b5251ff34e3a239726))
|
||||
* **CommandPalette:** command icons opacity in dark mode ([abb93b5](https://github.com/nuxtlabs/ui/commit/abb93b5fd3ddda8c91db3370c8e3109cff4a091d))
|
||||
* **CommandPalette:** fix groups computed ([9302b8d](https://github.com/nuxtlabs/ui/commit/9302b8d635c3ffb489142601a17a9878072c89fe))
|
||||
* **CommandPalette:** group items spacing ([32922de](https://github.com/nuxtlabs/ui/commit/32922def7deec5bee920a1fb1400449461315d0d))
|
||||
* **CommandPalette:** hack for reactivity ([b43394d](https://github.com/nuxtlabs/ui/commit/b43394ddc3ee795b56679f7076e0c80a1c496b2e))
|
||||
* **CommandPalette:** icon color on hover ([e4f148e](https://github.com/nuxtlabs/ui/commit/e4f148efa97adf52b1b5544ff6c349a4ac82a956))
|
||||
* **CommandPalette:** icon inactive opacity on dark mode ([5722a3a](https://github.com/nuxtlabs/ui/commit/5722a3ae62706229179b75d9291babd1c2039244))
|
||||
* **CommandPalette:** prevent empty active slot ([056ab30](https://github.com/nuxtlabs/ui/commit/056ab304745c3ba8dedbf9d6491839b9d620df88))
|
||||
* **CommandPalette:** prevent shortcuts to disappear on hover ([f87252f](https://github.com/nuxtlabs/ui/commit/f87252f05debda7c98f5ab8a9453e57efafaad0f))
|
||||
* **CommandPalette:** reactivity issue + improve results ([ec9f670](https://github.com/nuxtlabs/ui/commit/ec9f67094a51e3afde92f7924b8ee5d4e9053158)), closes [#95](https://github.com/nuxtlabs/ui/issues/95) [#96](https://github.com/nuxtlabs/ui/issues/96)
|
||||
* **CommandPalette:** truncate suffix ([aa242aa](https://github.com/nuxtlabs/ui/commit/aa242aa87d5ae834d838518efd530003fdde5e24))
|
||||
* default object options ([95c14a4](https://github.com/nuxtlabs/ui/commit/95c14a43600016bf405b557752fad289fb31154a))
|
||||
* **Dropdown:** increase timeout for hover mode ([7291942](https://github.com/nuxtlabs/ui/commit/72919425b6e84581ba3b854aec3348977b335a3f))
|
||||
* error in Popover and Dropdown ([541ed30](https://github.com/nuxtlabs/ui/commit/541ed304a0a4fa2646115547e03e44cf9e17c65e))
|
||||
* **icon:** hydratation warning when loading same icon twice ([#99](https://github.com/nuxtlabs/ui/issues/99)) ([d57647a](https://github.com/nuxtlabs/ui/commit/d57647a77a145ff6e81d3a71550e98e3eaf3a842))
|
||||
* load icons on mount rather than within setup ([#82](https://github.com/nuxtlabs/ui/issues/82)) ([62361bf](https://github.com/nuxtlabs/ui/commit/62361bfa8f77c2f3452af108f08434ba4c6ec4c5))
|
||||
* **Modal:** use object for `innerStyle` ([72dc0d0](https://github.com/nuxtlabs/ui/commit/72dc0d0d0c270b2dfbf2ba8a8eb03a04eb5dea9a))
|
||||
* **Notification:** improve placement with description ([945fec6](https://github.com/nuxtlabs/ui/commit/945fec62c2efa6baf7b32c8a85ba658dfd9311c9))
|
||||
* **Notification:** prevent error without timeout ([8a66f5e](https://github.com/nuxtlabs/ui/commit/8a66f5e9bf65ab04b8878f0d597e439b45b46bb3))
|
||||
* **Popover:** `inline-flex` on trigger button ([593573a](https://github.com/nuxtlabs/ui/commit/593573a286459b48fde8f49df2c2f1fc1dc98da6))
|
||||
* **SelectCustom:** avoid submitting to form when closing ([#83](https://github.com/nuxtlabs/ui/issues/83)) ([cf1b2cd](https://github.com/nuxtlabs/ui/commit/cf1b2cdd133233481da6e1ec47b49b7f012aa204))
|
||||
|
||||
### [0.0.3](https://github.com/nuxtlabs/ui/compare/v0.0.2...v0.0.3) (2022-07-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **AvatarGroup:** preset support ([#69](https://github.com/nuxtlabs/ui/issues/69)) ([00b9a08](https://github.com/nuxtlabs/ui/commit/00b9a0839b3fb19521080684cf3f4e46cf4c64e5))
|
||||
* **button:** Add black variant ([#34](https://github.com/nuxtlabs/ui/issues/34)) ([46ca8c5](https://github.com/nuxtlabs/ui/commit/46ca8c5422cc00a2c66376d910b9669ab82002d0))
|
||||
* **clipboard:** replace `navigator.clipboard` with vueuse `useClipboard` ([#33](https://github.com/nuxtlabs/ui/issues/33)) ([4532e09](https://github.com/nuxtlabs/ui/commit/4532e09ac067518fe607d7079b2761c783f36505))
|
||||
* **CommandPalette:** implement component ([18dceb7](https://github.com/nuxtlabs/ui/commit/18dceb74455ccd6690069b973bcc9f563c78ebb3))
|
||||
* **Dropdown:** add `hover` mode ([#45](https://github.com/nuxtlabs/ui/issues/45)) ([77149f0](https://github.com/nuxtlabs/ui/commit/77149f0dd454882dd0a271794d80a9a597789923))
|
||||
* **Icon:** support custom component and emoji ([c1a7629](https://github.com/nuxtlabs/ui/commit/c1a7629e2988886633f989244fc2abfd580c6e1f))
|
||||
* migrate to `@nuxtjs/tailwindcss` ([#32](https://github.com/nuxtlabs/ui/issues/32)) ([702abf7](https://github.com/nuxtlabs/ui/commit/702abf7a9fd91428d57b5dc95be34ded7f2db9e2))
|
||||
* **module:** handle variants with dynamic colors ([5a8b078](https://github.com/nuxtlabs/ui/commit/5a8b078d65e32d0d3f5b8dc8df903814abfd5fca))
|
||||
* **plugins:** clipboard ([#29](https://github.com/nuxtlabs/ui/issues/29)) ([832ffe4](https://github.com/nuxtlabs/ui/commit/832ffe4323c73e43506261c4c1765fd68851d7a0))
|
||||
* **Popover:** handle hovering mode ([#47](https://github.com/nuxtlabs/ui/issues/47)) ([c431f8b](https://github.com/nuxtlabs/ui/commit/c431f8b4a17880b31eb838059cb03c8e95955c0e))
|
||||
* **Slideover:** add close button in header ([#65](https://github.com/nuxtlabs/ui/issues/65)) ([2f90ce2](https://github.com/nuxtlabs/ui/commit/2f90ce2319766aed92063bcc9e3c92d7a1b3de99))
|
||||
* **Slideover:** allow opening from the right side ([#64](https://github.com/nuxtlabs/ui/issues/64)) ([aecfef2](https://github.com/nuxtlabs/ui/commit/aecfef20e6aa7a092741f4ab7988c6b3e04bd706))
|
||||
* **Slideover:** preset support ([#68](https://github.com/nuxtlabs/ui/issues/68)) ([5b4e4f8](https://github.com/nuxtlabs/ui/commit/5b4e4f864894261349834a5587c9eab4a0738c47))
|
||||
* **toast:** add aliases for `info` and `warning` notifications ([23deef3](https://github.com/nuxtlabs/ui/commit/23deef325a33bd52f90dbd0a1005131b4314c1fd))
|
||||
* **toast:** expose `timeout` to alias methods ([#30](https://github.com/nuxtlabs/ui/issues/30)) ([6bd5197](https://github.com/nuxtlabs/ui/commit/6bd51975bfefb963fa59c4a151833db029cfe93a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Avatar:** add missing `watch` import ([cc01af8](https://github.com/nuxtlabs/ui/commit/cc01af8e553bfe50eb8f1a51260db0331dc11a69))
|
||||
* **AvatarGroup:** pass all avatar props ([723f075](https://github.com/nuxtlabs/ui/commit/723f075c562fa5ed261d02656e7cd5684fa06170))
|
||||
* **Avatar:** missing `ref` import ([eb41b23](https://github.com/nuxtlabs/ui/commit/eb41b23432f931adaf52c650034e394c6ed9c547))
|
||||
* **Avatar:** placeholder ([#31](https://github.com/nuxtlabs/ui/issues/31)) ([1bec8d1](https://github.com/nuxtlabs/ui/commit/1bec8d163c9288ecbce5acbc13a71b7a2c43fc5e))
|
||||
* **Avatar:** prevent boolean src ([da3ed26](https://github.com/nuxtlabs/ui/commit/da3ed26c2ccc65f3b7d68fd07315c7c607e88318))
|
||||
* **Avatar:** remove gradient support ([ed499b3](https://github.com/nuxtlabs/ui/commit/ed499b3b21097ead948de6c5f4270ffda5dafb6e))
|
||||
* **Avatar:** truncate placeholder if too long ([#61](https://github.com/nuxtlabs/ui/issues/61)) ([6585bfc](https://github.com/nuxtlabs/ui/commit/6585bfc24a36d9b35bc1f5dcec9394c5aa731ec4))
|
||||
* **Avatar:** url error handling ([#39](https://github.com/nuxtlabs/ui/issues/39)) ([fb3ff2e](https://github.com/nuxtlabs/ui/commit/fb3ff2e5fa2fd0a86a71da40cf4a7258be0d3899))
|
||||
* **Button:** wrong config for icon size class ([760da3d](https://github.com/nuxtlabs/ui/commit/760da3d1a81ca06b49e95d30e7794ad3c72acb81))
|
||||
* **Card:** nullable validator on card roundedClass prop ([c4a40b0](https://github.com/nuxtlabs/ui/commit/c4a40b065c021e3af706ea65a02fdce591f5acb4))
|
||||
* **Card:** prevent double class ([06b07e2](https://github.com/nuxtlabs/ui/commit/06b07e292e9b29359926273fcacb6d25aa440f10))
|
||||
* **Card:** prevent empty `sm:` class when `rounded-class` is null ([dd64637](https://github.com/nuxtlabs/ui/commit/dd6463710c58b62a3a5714a5917d885bb668ff55))
|
||||
* **colors:** hard-code colors as tailwindcss/colors is different ([f67fdb7](https://github.com/nuxtlabs/ui/commit/f67fdb7d2fbe12f696cc02f00800dc0c4d8e5df5))
|
||||
* **colors:** move primary to safeColors ([3ab0698](https://github.com/nuxtlabs/ui/commit/3ab0698d7f2ce4776ccf4f16e069e0af2ccd38b8))
|
||||
* **CommandPalette:** add missing import ([ea293ba](https://github.com/nuxtlabs/ui/commit/ea293bae0ca10c19e8a1c021fd0d708933cd6c0b))
|
||||
* **CommandPaletteGroup:** fail replace on items ([1495ff9](https://github.com/nuxtlabs/ui/commit/1495ff987d4e464540755a4e2b7e72b0b91548dc))
|
||||
* **CommandPaletteGroup:** invalid spacing when no icon ([cf65b4a](https://github.com/nuxtlabs/ui/commit/cf65b4ab5456f2c5b54968ab011330fc324bad5f))
|
||||
* **CommandPalette:** options priority ([76ffbf4](https://github.com/nuxtlabs/ui/commit/76ffbf4cf355f3cb36a9d896a4ed6d2a874fe780))
|
||||
* **CommandPalette:** slice from computed options ([503b9a6](https://github.com/nuxtlabs/ui/commit/503b9a6b5c2e31152e7ea42aa059e595a4a92c17))
|
||||
* Dropdown and Popover manual padding ([2e09939](https://github.com/nuxtlabs/ui/commit/2e099392f1f9ab926c6ce42146f649496a4723c3))
|
||||
* **Dropdown:** improve disabled state ([5fb7f10](https://github.com/nuxtlabs/ui/commit/5fb7f102830eb33b207cf3ca0c955f7853df5fbd))
|
||||
* Hover mode on Dropdown & Popover ([#48](https://github.com/nuxtlabs/ui/issues/48)) ([8bc4902](https://github.com/nuxtlabs/ui/commit/8bc4902078b34c0233c9304c996d38746b14b270))
|
||||
* **Icon:** missing imports ([c3facb1](https://github.com/nuxtlabs/ui/commit/c3facb1fefa4c9681290d2f88ea2c78a7aeee217))
|
||||
* **Icon:** name can be an object ([f513ea6](https://github.com/nuxtlabs/ui/commit/f513ea6ca89928982694819abbc8b5f26ddec5ae))
|
||||
* **Icon:** reload icon when prop name changes ([78021d3](https://github.com/nuxtlabs/ui/commit/78021d385058634658f722ee90db214661ae88e4))
|
||||
* **input:** background should go into appearance ([33b1176](https://github.com/nuxtlabs/ui/commit/33b1176bdd66e3774e8063e1e2caef4983e7680d))
|
||||
* **Link:** `exact` handling ([ceedbe0](https://github.com/nuxtlabs/ui/commit/ceedbe0bbdb4cfac0548e3d00ccc2480bd41ec13))
|
||||
* **Link:** add missing `inactive-class` on `button` and `a` ([035919a](https://github.com/nuxtlabs/ui/commit/035919a545f20b2714cbc21a7efa2c7ae2ad16cb))
|
||||
* **Link:** handle `isActive` through vue-router ([aef1156](https://github.com/nuxtlabs/ui/commit/aef11562c945fe5e8384705c04948aa5f9db085e))
|
||||
* **Link:** send correct active ([d4b6599](https://github.com/nuxtlabs/ui/commit/d4b65996ce025795e05d4e2621713f2ca46f855f))
|
||||
* **Link:** use `exact` ([7737242](https://github.com/nuxtlabs/ui/commit/77372423d824dcfc7a175ccbeb3a6cb8a78a5af9))
|
||||
* **Modal:** `widthClass` prop and default preset value ([#56](https://github.com/nuxtlabs/ui/issues/56)) ([d980176](https://github.com/nuxtlabs/ui/commit/d980176e03b9853a1c7f26db7a06c238fec96b81))
|
||||
* **Modal:** move classes to `DialogPanel` ([dfe86f0](https://github.com/nuxtlabs/ui/commit/dfe86f0baf8ee43d4efc6730b8853dbcc322cce1))
|
||||
* **Modal:** prevent attrs inherit ([850c766](https://github.com/nuxtlabs/ui/commit/850c766fabaef5be27270338b1e76abacae48877))
|
||||
* **module:** `gradient-avatar` include ([85c8210](https://github.com/nuxtlabs/ui/commit/85c8210edeff59b18d3a46fb56fe8e5b8667a473))
|
||||
* **module:** import `colors` from preset-mini ([185273f](https://github.com/nuxtlabs/ui/commit/185273f3e91d8a1d175020fe2774a2d948105b87))
|
||||
* **module:** move colors utils to runtime dir ([93c9fe1](https://github.com/nuxtlabs/ui/commit/93c9fe1c74bc8ba02a6ef8f3bfc8a8b260349c93))
|
||||
* **module:** parse presets with mjs ext ([b2705fe](https://github.com/nuxtlabs/ui/commit/b2705feedce6a2c957990da8730c800cfd654d46))
|
||||
* **module:** presets content ([b8dd80d](https://github.com/nuxtlabs/ui/commit/b8dd80d3d37fbf9c02a8a056615deec8ffcd9302))
|
||||
* **module:** register typography plugin ([bbbe57d](https://github.com/nuxtlabs/ui/commit/bbbe57dedbab96fe08671488a91626f018f19c71))
|
||||
* **module:** remove safelist on max-w ([358387e](https://github.com/nuxtlabs/ui/commit/358387e3584b571685831171c322e6c98bd80433))
|
||||
* **module:** resolve runtime dir ([65aa169](https://github.com/nuxtlabs/ui/commit/65aa1690ba584c7c66685ae22968b5e3bb086aff))
|
||||
* **module:** search in tailwind colors for `primary` and `gray` ([10d89d3](https://github.com/nuxtlabs/ui/commit/10d89d3cd109561138f262c18732d832bae371dc))
|
||||
* **module:** use `variants` key for safelist ([4c89122](https://github.com/nuxtlabs/ui/commit/4c891225b18efbc039288af489929c73d5245d0b))
|
||||
* **Notifications:** default value in `useState` ([af566ab](https://github.com/nuxtlabs/ui/commit/af566ab1fa94d9542b0fd5f96010962054342085))
|
||||
* **Notifications:** inexistant `z-55` ([60c72a2](https://github.com/nuxtlabs/ui/commit/60c72a20d1fcb82734c13721b003cef9222ee61a))
|
||||
* **package:** update `postbuild:docs` command ([8045c7b](https://github.com/nuxtlabs/ui/commit/8045c7b47e117d1ec5d0eb0a53973743079d9f98))
|
||||
* **plugins:** error in provides ([46ea467](https://github.com/nuxtlabs/ui/commit/46ea467098441b96329383154e7e60fd8ebbec06))
|
||||
* **Popover:** add missing `onMounted` import ([656b6e1](https://github.com/nuxtlabs/ui/commit/656b6e1c59d89a82b2c3117c87a57e9c99e621c8))
|
||||
* **Popover:** add missing `ref` import ([2eb2fea](https://github.com/nuxtlabs/ui/commit/2eb2feab672da15f102a500dfbde036bef96b9e4))
|
||||
* **popper:** use `$el` after 1.5 upgrade ([9554e80](https://github.com/nuxtlabs/ui/commit/9554e801c26de31205f84459ae7679f714de172a)), closes [/github.com/tailwindlabs/headlessui/discussions/1125#discussioncomment-2299441](https://github.com/nuxtlabs//github.com/tailwindlabs/headlessui/discussions/1125/issues/discussioncomment-2299441)
|
||||
* **preset:** replace avatar wrapper with `inline-flex` ([45cf898](https://github.com/nuxtlabs/ui/commit/45cf898ec3f03a7cc8631e9fe2adc8bac8bd53e9))
|
||||
* **presets:** add disabled bg color on nuxt buttons ([e1d79d7](https://github.com/nuxtlabs/ui/commit/e1d79d7fe77e93305c04e7333effc2dad0d9b850))
|
||||
* **presets:** defu merging ([e034218](https://github.com/nuxtlabs/ui/commit/e0342184febf3dbd355189c0766b20d268fce6ef))
|
||||
* **presets:** dropdown avatar position ([516e7fa](https://github.com/nuxtlabs/ui/commit/516e7faf8ff0c8fe856da0c12ecae2d6d838b816))
|
||||
* **presets:** support dark ring-offset-color ([a9f1d93](https://github.com/nuxtlabs/ui/commit/a9f1d937bc18d590a9038b15aa21743a15b48b24))
|
||||
* **SelectCustom:** add default value to placeholder ([8492e16](https://github.com/nuxtlabs/ui/commit/8492e161dbb16bcece401da41ac83f250be8f68b))
|
||||
* **SelectCustom:** add missing `listContainerClass` prop ([ae6e8ee](https://github.com/nuxtlabs/ui/commit/ae6e8eec032b8fee5581692dcf9a9f873fc8300c))
|
||||
* **SelectCustom:** add missing `required` prop ([619f620](https://github.com/nuxtlabs/ui/commit/619f620b7ec9392b67c4ab61fb314dafd241b089))
|
||||
* **SelectCustom:** add missing `text-sm` class ([eb6fbd9](https://github.com/nuxtlabs/ui/commit/eb6fbd9c4ae9efcc5c608f4e26de857409d219b6))
|
||||
* **SelectCustom:** add missing bg in list input ([da4e8d5](https://github.com/nuxtlabs/ui/commit/da4e8d5c099dbde3a79d124d7debd590d8da0e58))
|
||||
* **SelectCustom:** add tabindex -1 to hidden input ([09aed4b](https://github.com/nuxtlabs/ui/commit/09aed4b7529bb40a4b1cf9e28cefcc0ba33e1e33))
|
||||
* **SelectCustom:** dark mode preset ([1e6ad72](https://github.com/nuxtlabs/ui/commit/1e6ad726441bb1f87c2688cdc203417bb0afe267))
|
||||
* **SelectCustom:** handle placeholder when value is null ([4e0d23e](https://github.com/nuxtlabs/ui/commit/4e0d23ed3470f79f5c6ab805a7f5edd39594c197))
|
||||
* **SelectCustom:** icon name in prop only for now ([2e64343](https://github.com/nuxtlabs/ui/commit/2e6434361004e6bc7e6de1e7ed669b719e5352d1))
|
||||
* **SelectCustom:** improve creatable placeholder ([d413cf7](https://github.com/nuxtlabs/ui/commit/d413cf74d6eed70870c60120c1cebbe0335becd6))
|
||||
* **SelectCustom:** missing padding on list ([524f841](https://github.com/nuxtlabs/ui/commit/524f8411c591fe68c54580d39c094e82cfe82620))
|
||||
* **SelectCustom:** move max-height on base ([db39a9c](https://github.com/nuxtlabs/ui/commit/db39a9cdba2a585a4bde6c26c8dae1e0b3c1cd02))
|
||||
* **SelectCustom:** move wrapper on top of `Listbox` ([8222d05](https://github.com/nuxtlabs/ui/commit/8222d05c15a7cede4f42eeebd2f3e14e01238819))
|
||||
* **SelectCustom:** move wrapper under `Listbox` ([936d6a5](https://github.com/nuxtlabs/ui/commit/936d6a5fee41fc628f4b4efb2a32d46088316be8))
|
||||
* **SelectCustom:** prop is `icon` instead of `iconName` ([6da0c28](https://github.com/nuxtlabs/ui/commit/6da0c2801909d27b0318c80f1ef0880150e4c14e))
|
||||
* **SelectCustom:** remove unused import ([0e0f3e3](https://github.com/nuxtlabs/ui/commit/0e0f3e39d31bd70404600ac7d4b9641335839f3c))
|
||||
* **Select:** default value handling ([eb00439](https://github.com/nuxtlabs/ui/commit/eb0043914bc832be57e1af6def72574a86c76339))
|
||||
* **Select:** disable placeholder ([7723704](https://github.com/nuxtlabs/ui/commit/7723704f793eb444b24ee8b9d0a0eb2260b991c0))
|
||||
* **Select:** normalizedValue handling Object modelValue ([#59](https://github.com/nuxtlabs/ui/issues/59)) ([e419d68](https://github.com/nuxtlabs/ui/commit/e419d68f645a14045519a72427f48015e331859b))
|
||||
* **Slideover:** remove useless padding ([cf021a5](https://github.com/nuxtlabs/ui/commit/cf021a5888af2d996ccdfd99e693ce6797c7d36a))
|
||||
* **Textarea:** autoresize ([#43](https://github.com/nuxtlabs/ui/issues/43)) ([ba643d9](https://github.com/nuxtlabs/ui/commit/ba643d9faa3761c336e18bfb4671db6e21e88397))
|
||||
* **textarea:** autoresize reactivity ([#52](https://github.com/nuxtlabs/ui/issues/52)) ([f0bfe20](https://github.com/nuxtlabs/ui/commit/f0bfe20572b028747ed04f3b185880e4204ea072))
|
||||
* **toast:** `id` should be a string ([7db0ca5](https://github.com/nuxtlabs/ui/commit/7db0ca50a00db12f9d5b3c9adf4d8ce788fe4d1b))
|
||||
* **Toggle:** add `v-if` when icon props not defined ([90ff1c0](https://github.com/nuxtlabs/ui/commit/90ff1c0671e53ffe7fbf7b018f077f8d8fbe16cc))
|
||||
* **Tooltip:** prevent close when hovering ([18c194e](https://github.com/nuxtlabs/ui/commit/18c194e8393c07acce40f2dc588d310b2f915126))
|
||||
* **useTimer:** lint rule changed ([0e18526](https://github.com/nuxtlabs/ui/commit/0e18526a6fa3e32b2aedd137ca9b57d326ea20d9))
|
||||
* **utils:** types ([00e0ab3](https://github.com/nuxtlabs/ui/commit/00e0ab39f8da9ffb6fffdd7121a69f3aaa071fd3))
|
||||
* **VerticalNavigation:** `link.avatar` is now an object ([d733e25](https://github.com/nuxtlabs/ui/commit/d733e25bf01ae083e06b35a1bbd58a8bc87f8c4b))
|
||||
* **VerticalNavigation:** remove `avatarSize` prop ([f0835cf](https://github.com/nuxtlabs/ui/commit/f0835cf979ff0e4e2fc1e36b1cbd377ee32f9b1b))
|
||||
|
||||
### 0.0.2 (2022-02-01)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @nuxthq/ui
|
||||
|
||||
Components library as a Nuxt3 module using [UnoCSS](https://github.com/antfu/unocss).
|
||||
Components library as a Nuxt module using [TailwindCSS](https://tailwindcss.com).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -11,7 +11,7 @@ yarn add --dev @nuxthq/ui
|
||||
Then, register the module in your `nuxt.config.js`:
|
||||
|
||||
```js
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
buildModules: [
|
||||
@@ -39,7 +39,7 @@ Define the primary variant. Defaults to `indigo`. You can specify your own objec
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
buildModules: [
|
||||
@@ -58,7 +58,7 @@ Define the prefix of the imported components. Defaults to `u`.
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
buildModules: [
|
||||
|
||||
12
build.config.ts
Normal file
12
build.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { defineBuildConfig } from 'unbuild'
|
||||
|
||||
export default defineBuildConfig({
|
||||
hooks: {
|
||||
'rollup:done': async (ctx) => {
|
||||
// copy env.d.ts to dist
|
||||
await fs.copyFile(resolve('src/env.d.ts'), join(ctx.options.outDir, 'env.d.ts'))
|
||||
}
|
||||
}
|
||||
})
|
||||
42
docs/app.vue
42
docs/app.vue
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="antialiased font-sans">
|
||||
<nav class="u-bg-white border-b u-border-gray-200 fixed top-0 inset-x-0 z-50">
|
||||
<div>
|
||||
<nav class="u-bg-white border-b u-border-gray-200 fixed top-0 inset-x-0 z-10">
|
||||
<UContainer padded>
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
@@ -9,16 +9,19 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<UseDark v-slot="{ isDark, toggleDark }">
|
||||
<UButton variant="transparent" :icon="isDark ? 'heroicons-outline:moon' : 'heroicons-outline:sun'" @click="toggleDark()" />
|
||||
</UseDark>
|
||||
<div class="flex items-center gap-3">
|
||||
<ColorScheme placeholder="" tag="span">
|
||||
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'heroicons-outline:moon' : 'heroicons-outline:sun'" @click="toggleDark" />
|
||||
</ColorScheme>
|
||||
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="fa-brands:github" />
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</nav>
|
||||
|
||||
<UContainer class="mt-16">
|
||||
<div class="lg:grid lg:grid-cols-10 lg:gap-10 lg:relative">
|
||||
<aside class="lg:flex lg:flex-col lg:relative pb-8 lg:pb-0 lg:sticky lg:top-0 px-4 sm:px-6 lg:px-0 lg:pt-16 lg:-mt-16 lg:self-start lg:col-span-2 lg:overflow-hidden lg:sticky lg:h-screen">
|
||||
<aside class="lg:flex lg:flex-col pb-8 lg:pb-0 lg:sticky lg:top-0 px-4 sm:px-6 lg:px-0 lg:pt-16 lg:-mt-16 lg:self-start lg:col-span-2 lg:overflow-hidden lg:h-screen">
|
||||
<nav class="overflow-y-auto h-auto pt-8 lg:py-12">
|
||||
<ul class="space-y-6">
|
||||
<li v-for="section of sections" :key="section">
|
||||
@@ -56,22 +59,37 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { UseDark } from '@vueuse/components'
|
||||
useHead({
|
||||
title: '@nuxthq/ui',
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }
|
||||
],
|
||||
bodyAttrs: {
|
||||
class: 'antialiased font-sans text-gray-700 bg-gray-50 dark:bg-gray-900 dark:text-gray-200 bg-white dark:bg-black'
|
||||
}
|
||||
})
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const toggleDark = () => {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ label: 'Getting Started', links: [{ label: 'Usage', to: '/' }, { label: 'Examples', to: '/examples' }, { label: 'Migration', to: '/migration' }, { label: 'Dark mode', to: '/dark' }] },
|
||||
{ label: 'Elements', links: [{ label: 'Avatar', to: '/components/Avatar' }, { label: 'AvatarGroup', to: '/components/AvatarGroup' }, { label: 'Badge', to: '/components/Badge' }, { label: 'Button', to: '/components/Button' }, { label: 'Dropdown', to: '/components/Dropdown' }, { label: 'Icon', to: '/components/Icon' }] },
|
||||
{ label: 'Feedback', links: [{ label: 'Alert', to: '/components/Alert' }, { label: 'AlertDialog', to: '/components/AlertDialog' }] },
|
||||
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'FormGroup', to: '/components/FormGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'Select', to: '/components/Select' }, { label: 'Textarea', to: '/components/Textarea' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
||||
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'FormGroup', to: '/components/FormGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
||||
{ label: 'Layout', links: [{ label: 'Card', to: '/components/Card' }, { label: 'Container', to: '/components/Container' }] },
|
||||
// { label: 'Navigation', links: [{ label: 'Pills', to: '/components/Pills' }, { label: 'Tabs', to: '/components/Tabs' }, { label: 'VerticalNavigation', to: '/components/VerticalNavigation' }] },
|
||||
{ label: 'Navigation', links: [{ label: 'VerticalNavigation', to: '/components/VerticalNavigation' }] },
|
||||
{ label: 'Overlays', links: [{ label: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
|
||||
{ label: 'Navigation', links: [{ label: 'Pills', to: '/components/Pills' }, { label: 'Tabs', to: '/components/Tabs' }, { label: 'VerticalNavigation', to: '/components/VerticalNavigation' }, { label: 'CommandPalette', to: '/components/CommandPalette' }] },
|
||||
{ label: 'Overlays', links: [{ label: 'Modal', to: '/components/Modal' }, { label: 'Notification', to: '/components/Notification' }, { label: 'Popover', to: '/components/Popover' }, { label: 'Slideover', to: '/components/Slideover' }, { label: 'Tooltip', to: '/components/Tooltip' }] }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html.dark {
|
||||
background-color: black;
|
||||
@apply bg-black;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,27 @@
|
||||
/* @unocss-include */
|
||||
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import module from '../src/module'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
import nuxtUI from '../src/module'
|
||||
|
||||
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
meta: {
|
||||
title: '@nuxthq/ui',
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }
|
||||
],
|
||||
htmlAttrs: {
|
||||
class: 'u-bg-white'
|
||||
},
|
||||
bodyAttrs: {
|
||||
class: 'u-bg-gray-50 u-text-gray-700'
|
||||
}
|
||||
},
|
||||
buildModules: [
|
||||
module
|
||||
modules: [
|
||||
nuxtUI
|
||||
],
|
||||
components: {
|
||||
global: true
|
||||
},
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue'
|
||||
primary: 'blue',
|
||||
gray: 'zinc'
|
||||
},
|
||||
preset: {
|
||||
container: {
|
||||
constrained: 'max-w-8xl'
|
||||
}
|
||||
},
|
||||
unocss: {
|
||||
tailwindcss: {
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: '"Inter var", sans-serif'
|
||||
},
|
||||
maxWidth: {
|
||||
'8xl': '90rem'
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter var', ...defaultTheme.fontFamily.sans]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<template>
|
||||
<UCard v-if="component" class="relative flex flex-col lg:h-[calc(100vh-10rem)]" body-class="px-4 py-5 sm:p-6 relative" footer-class="flex flex-col flex-1 overflow-hidden">
|
||||
<UCard v-if="component" class="relative flex flex-col" body-class="px-4 py-5 sm:p-6 relative" footer-class="flex flex-col flex-1 overflow-hidden">
|
||||
<div class="flex justify-center">
|
||||
<component :is="`U${defaultProps[params.component].component.name}`" v-if="defaultProps[params.component] && defaultProps[params.component].component" v-bind="defaultProps[params.component].component.props" />
|
||||
|
||||
@@ -8,24 +11,40 @@
|
||||
<template v-if="Array.isArray(slot)">
|
||||
<div :key="key">
|
||||
<component
|
||||
:is="slot.component ? `U${slot.component}` : slot.tag"
|
||||
:is="slot.component ? `U${slot.component.name}` : slot.tag"
|
||||
v-for="(slot, index) of slot"
|
||||
:key="index"
|
||||
:class="slot.class"
|
||||
v-bind="slot.props || defaultProps[slot.component]"
|
||||
v-bind="slot.component?.props || defaultProps[slot.component]"
|
||||
v-html="slot.html"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<component :is="slot.component ? `U${slot.component}` : slot.tag" :key="key" :class="slot.class" v-bind="slot.props || defaultProps[slot.component]" v-html="slot.html" />
|
||||
<component :is="`U${slot.component.name}`" v-if="slot.component" :key="`${key}-component`" v-bind="slot.component?.props || defaultProps[slot.component]" />
|
||||
<component :is="slot.tag" v-else :key="`${key}-tag`" :class="slot.class" v-html="slot.html" />
|
||||
</template>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<template v-if="props.length" #footer>
|
||||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3 lg:overflow-y-auto">
|
||||
<div class="border-b u-border-gray-200">
|
||||
<pre class="text-sm leading-6 u-text-gray-900 flex-1 relative flex ligatures-none overflow-x-hidden px-4 sm:px-6 py-5 sm:py-6">
|
||||
<code class="flex-none min-w-full whitespace-pre-wrap break-all">{{ code }}</code>
|
||||
|
||||
<UButton
|
||||
class="absolute top-0 right-0"
|
||||
:icon="copied ? 'heroicons-outline:clipboard-check' : 'heroicons-outline:clipboard-copy'"
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
:custom-class="copied ? '!text-green-500' : ''"
|
||||
@click="onCopy"
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
||||
<UFormGroup
|
||||
v-for="prop of props"
|
||||
:key="prop.key"
|
||||
@@ -63,34 +82,21 @@
|
||||
/>
|
||||
<UTextarea
|
||||
v-else
|
||||
v-model="prop.value"
|
||||
:model-value="prop.value && JSON.stringify(prop.value)"
|
||||
:name="prop.key"
|
||||
size="sm"
|
||||
:rows="8"
|
||||
autoresize
|
||||
@update:model-value="value => prop.value = JSON.parse(value)"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<div class="border-t u-border-gray-200 u-bg-gray-50">
|
||||
<pre class="text-sm leading-6 u-text-gray-900 flex-1 relative flex ligatures-none lg:overflow-y-auto overflow-x-hidden px-4 sm:px-6 py-5 sm:py-6">
|
||||
<code class="flex-none min-w-full whitespace-pre-wrap break-all">{{ code }}</code>
|
||||
|
||||
<UButton
|
||||
class="absolute top-0 right-0"
|
||||
:icon="copied ? 'heroicons-outline:clipboard-check' : 'heroicons-outline:clipboard-copy'"
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
:custom-class="copied ? '!text-green-500' : ''"
|
||||
@click="onCopy"
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
@@ -100,9 +106,20 @@ const is = `U${params.component}`
|
||||
|
||||
const component = nuxtApp.vueApp.component(is)
|
||||
|
||||
const people = [
|
||||
{ id: 1, label: 'Durward Reynolds', disabled: false },
|
||||
{ id: 2, label: 'Kenton Towne', disabled: false },
|
||||
{ id: 3, label: 'Therese Wunsch', disabled: false },
|
||||
{ id: 4, label: 'Benedict Kessler', disabled: true },
|
||||
{ id: 5, label: 'Katelyn Rohan', disabled: false }
|
||||
]
|
||||
|
||||
const selectCustom = ref(people[0])
|
||||
const commandPalette = ref(people[0])
|
||||
const alertDialog = ref(false)
|
||||
const toggle = ref(false)
|
||||
const modal = ref(false)
|
||||
const slideover = ref(false)
|
||||
|
||||
const defaultProps = {
|
||||
Button: {
|
||||
@@ -180,6 +197,15 @@ const defaultProps = {
|
||||
}
|
||||
]
|
||||
},
|
||||
CommandPalette: {
|
||||
modelValue: commandPalette,
|
||||
'onUpdate:modelValue': (v) => { commandPalette.value = v },
|
||||
groups: [{
|
||||
key: 'people',
|
||||
label: 'People',
|
||||
commands: people
|
||||
}]
|
||||
},
|
||||
Icon: {
|
||||
name: 'heroicons-outline:bell'
|
||||
},
|
||||
@@ -192,10 +218,12 @@ const defaultProps = {
|
||||
label: 'Input group',
|
||||
slots: {
|
||||
default: {
|
||||
component: 'Input',
|
||||
props: {
|
||||
name: 'input',
|
||||
placeholder: 'Works with every form element'
|
||||
component: {
|
||||
name: 'Input',
|
||||
props: {
|
||||
name: 'input',
|
||||
placeholder: 'Works with every form element'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +242,12 @@ const defaultProps = {
|
||||
name: 'select',
|
||||
options: ['English', 'Spanish', 'French', 'German', 'Chinese']
|
||||
},
|
||||
SelectCustom: {
|
||||
modelValue: selectCustom,
|
||||
'onUpdate:modelValue': (v) => { selectCustom.value = v },
|
||||
textAttribute: 'label',
|
||||
options: people
|
||||
},
|
||||
Textarea: {
|
||||
name: 'textarea'
|
||||
},
|
||||
@@ -242,14 +276,34 @@ const defaultProps = {
|
||||
html: 'Modal content'
|
||||
},
|
||||
footer: {
|
||||
component: 'Button',
|
||||
props: {
|
||||
label: 'Close',
|
||||
onClick: () => { modal.value = false }
|
||||
component: {
|
||||
name: 'Button',
|
||||
props: {
|
||||
label: 'Close',
|
||||
onClick: () => { modal.value = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Slideover: {
|
||||
modelValue: slideover,
|
||||
'onUpdate:modelValue': (v) => { slideover.value = v },
|
||||
component: {
|
||||
name: 'Button',
|
||||
props: {
|
||||
variant: 'secondary',
|
||||
label: 'Open slideover',
|
||||
onClick: () => { slideover.value = true }
|
||||
}
|
||||
},
|
||||
slots: {
|
||||
default: {
|
||||
tag: 'div',
|
||||
html: 'Slideover content'
|
||||
}
|
||||
}
|
||||
},
|
||||
Popover: {
|
||||
slots: {
|
||||
panel: {
|
||||
@@ -258,12 +312,48 @@ const defaultProps = {
|
||||
html: 'Popover content'
|
||||
}
|
||||
}
|
||||
},
|
||||
Tabs: {
|
||||
links: [{
|
||||
label: 'Usage',
|
||||
to: '/',
|
||||
exact: true
|
||||
}, {
|
||||
label: 'Examples',
|
||||
to: '/examples'
|
||||
}, {
|
||||
label: 'Migration',
|
||||
to: '/migration'
|
||||
}, {
|
||||
label: 'Tabs',
|
||||
to: '/components/Tabs'
|
||||
}]
|
||||
},
|
||||
Pills: {
|
||||
links: [{
|
||||
label: 'Usage',
|
||||
to: '/',
|
||||
exact: true
|
||||
}, {
|
||||
label: 'Examples',
|
||||
to: '/examples'
|
||||
}, {
|
||||
label: 'Migration',
|
||||
to: '/migration'
|
||||
}, {
|
||||
label: 'Pills',
|
||||
to: '/components/Pills'
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const componentDefaultProps = defaultProps[params.component] || {}
|
||||
const { props: componentProps } = await component.__asyncLoader()
|
||||
|
||||
function lowercaseFirstLetter (string) {
|
||||
return string.charAt(0).toLowerCase() + string.slice(1)
|
||||
}
|
||||
|
||||
const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
||||
const defaultValue = componentDefaultProps[key]
|
||||
const propDefault = (typeof prop.default === 'function' ? prop.default() : prop.default)
|
||||
@@ -281,7 +371,7 @@ const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
||||
if (arrayRegex) {
|
||||
values = JSON.parse(arrayRegex[0].replace(/'/g, '"')).filter(Boolean)
|
||||
} else {
|
||||
const $uiProp = $ui[params.component.toLowerCase()][key]
|
||||
const $uiProp = $ui[lowercaseFirstLetter(params.component)][key]
|
||||
if ($uiProp) {
|
||||
values = Object.keys($uiProp).filter(Boolean)
|
||||
}
|
||||
@@ -289,7 +379,7 @@ const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
||||
}
|
||||
|
||||
if (value) {
|
||||
if (type === 'String') {
|
||||
if (type === 'String' && typeof value === 'string') {
|
||||
value = value.replace(/^'(.*)'$/, '$1')
|
||||
} else if (type === 'Array') {
|
||||
value = JSON.stringify(value)
|
||||
@@ -307,6 +397,7 @@ const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
||||
|
||||
const eventProps = Object.entries(componentDefaultProps)
|
||||
.filter(([key]) => !refProps.find(prop => prop.key === key))
|
||||
.filter(([key]) => !['slots'].includes(key))
|
||||
.reduce((acc, [key, value]) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
@@ -343,14 +434,9 @@ function toKebabCase (str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
const copied = ref(false)
|
||||
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(code.value).then(() => {
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
})
|
||||
copy(code.value)
|
||||
}
|
||||
|
||||
const code = computed(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-lg u-text-gray-500">
|
||||
Dark mode implementation with <a href="https://vueuse.org" target="_blank" class="underline">VueUse</a>.
|
||||
Dark mode implementation with <a href="https://color-mode.nuxtjs.org/" target="_blank" class="underline">Color Mode</a> module.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,13 +16,7 @@
|
||||
Usage
|
||||
</h2>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
{{ code5 }}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>The VueUse <a href="https://vueuse.org/core/useDark" target="_blank" class="underline">useDark</a> composable makes use of the `dark` class on the html tag so you can easily take advantage of the UnoCSS `dark` variant.</p>
|
||||
<p>TailwindCSS takes advantage of the `dark` class on the html tag:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
@@ -30,13 +24,8 @@
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>You can implement a toggle button easily by doing:</p>
|
||||
<p>The `@nuxtjs/color-mode` module is now installed by default, you can easily implement a toggle button:</p>
|
||||
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
{{ code2 }}
|
||||
</code>
|
||||
</pre>
|
||||
<pre class="u-bg-gray-900 rounded-md u-text-white px-4">
|
||||
<code class="text-sm">
|
||||
{{ code1 }}
|
||||
@@ -63,19 +52,14 @@
|
||||
|
||||
<script setup>
|
||||
const code1 = `
|
||||
<UseDark v-slot="{ isDark, toggleDark }">
|
||||
<UButton variant="transparent" :icon="isDark ? 'heroicons-outline:moon' : 'heroicons-outline:sun'" @click="toggleDark()" />
|
||||
</UseDark>`
|
||||
const code2 = `
|
||||
import { UseDark } from '@vueuse/components'
|
||||
`
|
||||
const colorMode = useColorMode()
|
||||
const toggleDark = () => {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}`
|
||||
const code3 = `
|
||||
<div class="u-bg-gray-100 border u-border-gray-200 u-text-gray-700"></div>
|
||||
`
|
||||
const code4 = `
|
||||
<div class="bg-white dark:bg-black"></div>
|
||||
`
|
||||
const code5 = `
|
||||
yarn add @vueuse/components @vueuse/core
|
||||
`
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="pb-10 border-b u-border-gray-200 mb-10">
|
||||
@@ -39,7 +40,7 @@
|
||||
Toggle modal!
|
||||
</UButton>
|
||||
|
||||
<UModal v-model="isModalOpen">
|
||||
<UModal v-model="isModalOpen" @submit.prevent="onSubmit">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<UIcon name="heroicons-outline:exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
@@ -56,7 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" @click="open = false">
|
||||
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" @click="open = false">
|
||||
Deactivate
|
||||
</button>
|
||||
<button ref="cancelButtonRef" type="button" class="mt-3 w-full inline-flex justify-center rounded-md border u-border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium u-text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm" @click="open = false">
|
||||
@@ -87,11 +88,6 @@
|
||||
<button class="flex">
|
||||
<UAvatar src="https://picsum.photos/200/300" />
|
||||
</button>
|
||||
|
||||
<template #item-with-avatar="{ item }">
|
||||
<UAvatar v-if="item.avatar" :src="item.avatar" size="xxs" class="mr-3" />
|
||||
{{ item.label }}
|
||||
</template>
|
||||
</UDropdown>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +96,7 @@
|
||||
Popover:
|
||||
</div>
|
||||
|
||||
<UPopover panel-class="w-screen max-w-sm px-4 sm:px-0 transform" wrapper-class="inline-block relative">
|
||||
<UPopover mode="hover" wrapper-class="inline-block relative">
|
||||
<template #default="{ open }">
|
||||
<UButton variant="secondary" :icon="open ? 'heroicons-solid:chevron-up' : 'heroicons-solid:chevron-down'" trailing icon-class="transition">
|
||||
Open popover!
|
||||
@@ -158,8 +154,8 @@
|
||||
Tooltip:
|
||||
</div>
|
||||
|
||||
<UTooltip text="Hello tooltip!">
|
||||
<UIcon name="heroicons-outline:information-circle" class="w-6 h-6 text-black cursor-pointer" />
|
||||
<UTooltip text="Hello tooltip!" :shortcuts="['⌘', 'G']">
|
||||
<UIcon name="heroicons-outline:information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
@@ -170,21 +166,75 @@
|
||||
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Context menu:
|
||||
</div>
|
||||
|
||||
<UCard ref="contextMenuRef" class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
|
||||
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
|
||||
<UCard @click.stop>
|
||||
Menu
|
||||
</UCard>
|
||||
</UContextMenu>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Command palette:
|
||||
</div>
|
||||
|
||||
<UCard body-class="">
|
||||
<UCommandPalette
|
||||
v-model="form.persons"
|
||||
multiple
|
||||
:placeholder="false"
|
||||
:options="{
|
||||
fuseOptions: {
|
||||
includeMatches: true
|
||||
}
|
||||
}"
|
||||
:groups="[{
|
||||
key: 'persons',
|
||||
commands: people
|
||||
}]"
|
||||
command-attribute="name"
|
||||
/>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||
Card:
|
||||
</div>
|
||||
|
||||
<UCard body-class="flex" @submit.prevent="onSubmit">
|
||||
<UCard body-class="flex">
|
||||
<div class="flex-1 px-4 py-5 sm:p-6 space-y-3">
|
||||
<UFormGroup label="Email" name="email" required>
|
||||
<UInput v-model="form.email" type="email" name="email" required />
|
||||
<UInput v-model="form.email" type="email" name="email" required icon="heroicons-outline:mail" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Description" name="description">
|
||||
<UTextarea v-model="form.description" type="description" name="description" autoresize />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Person" name="person" required>
|
||||
<USelect
|
||||
v-model="form.personId"
|
||||
name="person"
|
||||
:options="people"
|
||||
placeholder="Select a person"
|
||||
text-attribute="name"
|
||||
value-attribute="id"
|
||||
icon="heroicons-outline:user"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="People" name="people" required>
|
||||
<USelectCustom v-model="form.person" name="people" :options="people" text-attribute="name" searchable />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Toggle" name="toggle">
|
||||
<UToggle v-model="form.toggle" name="toggle" icon-off="heroicons-solid:x" icon-on="heroicons-solid:check" />
|
||||
</UFormGroup>
|
||||
@@ -223,17 +273,56 @@
|
||||
|
||||
<script setup>
|
||||
const isModalOpen = ref(false)
|
||||
|
||||
const people = ref([
|
||||
{ id: 1, name: 'Durward Reynolds', disabled: false },
|
||||
{ id: 2, name: 'Kenton Towne', disabled: false },
|
||||
{ id: 3, name: 'Therese Wunsch', disabled: false },
|
||||
{ id: 4, name: 'Benedict Kessler', disabled: true },
|
||||
{ id: 5, name: 'Katelyn Rohan', disabled: false }
|
||||
])
|
||||
|
||||
const form = reactive({
|
||||
email: '',
|
||||
description: '',
|
||||
toggle: false,
|
||||
notification: 'email',
|
||||
notifications: [],
|
||||
terms: false
|
||||
terms: false,
|
||||
personId: null,
|
||||
person: ref(people.value[0]),
|
||||
persons: ref([people.value[0]])
|
||||
})
|
||||
|
||||
const { $toast } = useNuxtApp()
|
||||
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
const isContextMenuOpen = ref(false)
|
||||
const contextMenuRef = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||
x.value = clientX
|
||||
y.value = clientY
|
||||
})
|
||||
})
|
||||
|
||||
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
|
||||
|
||||
function openContextMenu () {
|
||||
const top = unref(y)
|
||||
const left = unref(x)
|
||||
|
||||
virtualElement.value.getBoundingClientRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top,
|
||||
left
|
||||
})
|
||||
isContextMenuOpen.value = true
|
||||
}
|
||||
|
||||
function toggleModalIsOpen () {
|
||||
isModalOpen.value = !isModalOpen.value
|
||||
}
|
||||
@@ -252,7 +341,10 @@ const dropdownItems = [
|
||||
[{
|
||||
label: 'Edit',
|
||||
icon: 'heroicons-solid:pencil',
|
||||
click: () => onClick()
|
||||
click: (e) => {
|
||||
e.preventDefault()
|
||||
onClick()
|
||||
}
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
icon: 'heroicons-solid:duplicate'
|
||||
@@ -262,7 +354,9 @@ const dropdownItems = [
|
||||
icon: 'heroicons-solid:archive'
|
||||
}, {
|
||||
label: 'Move',
|
||||
icon: 'heroicons-solid:external-link'
|
||||
icon: 'heroicons-solid:external-link',
|
||||
to: 'https://www.google.fr',
|
||||
target: '_blank'
|
||||
}],
|
||||
[{
|
||||
label: 'Delete',
|
||||
@@ -273,10 +367,9 @@ const dropdownItems = [
|
||||
const customDropdownItems = [
|
||||
[{
|
||||
label: 'benjamincanac',
|
||||
avatar: 'https://picsum.photos/200/300',
|
||||
avatar: { src: 'https://picsum.photos/200/300' },
|
||||
href: 'https://google.fr',
|
||||
target: '_blank',
|
||||
slot: 'item-with-avatar'
|
||||
target: '_blank'
|
||||
}],
|
||||
[{
|
||||
label: 'About',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="pb-10 border-b u-border-gray-200 mb-10">
|
||||
@@ -8,7 +9,7 @@
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-lg u-text-gray-500">
|
||||
Components library as a Nuxt3 module using <a href="https://github.com/antfu/unocss" target="_blank" class="underline">UnoCSS</a>.
|
||||
Components library as a Nuxt3 module using <a href="https://tailwindcss.com" target="_blank" class="underline">TailwindCSS</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -66,23 +67,11 @@
|
||||
|
||||
<p>- `colors.gray`</p>
|
||||
|
||||
<p>Define the gray variant. Defaults to `zinc`. You can like the `primary` color specify your own object. https://github.com/antfu/unocss/blob/main/packages/preset-uno/src/theme/colors.ts</p>
|
||||
<p>Define the gray variant. Defaults to `zinc`. You can like the `primary` color specify your own object. https://tailwindcss.com/docs/customizing-colors#default-color-palette</p>
|
||||
|
||||
<p>- `unocss.shortcuts`. Defaults to `[]`.</p>
|
||||
<p>- `tailwindcss.theme`. Defaults to `{}`.</p>
|
||||
|
||||
<p>Define UnoCSS shortcuts: https://github.com/antfu/unocss#shortcuts.</p>
|
||||
|
||||
<p>- `unocss.rules`. Defaults to `[]`.</p>
|
||||
|
||||
<p>Customize UnoCSS rules: https://github.com/antfu/unocss#custom-rules.</p>
|
||||
|
||||
<p>- `unocss.variants`. Defaults to `[]`.</p>
|
||||
|
||||
<p>Customize UnoCSS variants: https://github.com/antfu/unocss#custom-variants.</p>
|
||||
|
||||
<p>- `unocss.theme`. Defaults to `{}`.</p>
|
||||
|
||||
<p>Extend UnoCSS theme: https://github.com/antfu/unocss#extend-theme.</p>
|
||||
<p>Define TailwindCSS theme: https://tailwindcss.com/docs/theme.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -91,7 +80,7 @@ const code1 = `
|
||||
yarn add --dev @nuxthq/ui`
|
||||
|
||||
const code2 = `
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
defineNuxtConfig({
|
||||
buildModules: [
|
||||
@@ -107,7 +96,7 @@ const code3 = `
|
||||
}`
|
||||
|
||||
const code4 = `
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
defineNuxtConfig({
|
||||
ui: {
|
||||
@@ -116,7 +105,7 @@ defineNuxtConfig({
|
||||
})`
|
||||
|
||||
const code5 = `
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
import { defineNuxtConfig } from 'nuxt'
|
||||
|
||||
defineNuxtConfig({
|
||||
ui: {
|
||||
|
||||
@@ -68,167 +68,225 @@ const components = [
|
||||
{
|
||||
label: 'Avatar',
|
||||
to: '/components/Avatar',
|
||||
nuxt3: true
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'AvatarGroup',
|
||||
to: '/components/AvatarGroup',
|
||||
nuxt3: true
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Badge',
|
||||
to: '/components/Badge',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Button',
|
||||
to: '/components/Button',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Dropdown',
|
||||
to: '/components/Dropdown',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Icon',
|
||||
to: '/components/Icon',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Link',
|
||||
to: '/components/Link',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Toggle',
|
||||
to: '/components/Toggle',
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Alert',
|
||||
to: '/components/Alert',
|
||||
nuxt3: true
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'AlertDialog',
|
||||
to: '/components/AlertDialog',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Input',
|
||||
to: '/components/Input',
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'FormGroup',
|
||||
to: '/components/FormGroup',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Checkbox',
|
||||
to: '/components/Checkbox',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Radio',
|
||||
to: '/components/Radio',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Select',
|
||||
to: '/components/Select',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'SelectCustom',
|
||||
to: '/components/SelectCustom'
|
||||
to: '/components/SelectCustom',
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Textarea',
|
||||
to: '/components/Textarea',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Card',
|
||||
to: '/components/Card',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Container',
|
||||
to: '/components/Container',
|
||||
nuxt3: true,
|
||||
preset: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Pills',
|
||||
to: '/components/Pills'
|
||||
label: 'CommandPalette',
|
||||
to: '/components/CommandPalette',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: false,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Tabs',
|
||||
to: '/components/Tabs'
|
||||
to: '/components/Tabs',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Pills',
|
||||
to: '/components/Pills',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'VerticalNavigation',
|
||||
to: '/components/VerticalNavigation',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Modal',
|
||||
to: '/components/Modal',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
preset: true,
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Notification',
|
||||
to: '/components/Notification',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
to: '/components/Notifications',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Popover',
|
||||
to: '/components/Popover',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Slideover',
|
||||
to: '/components/Slideover',
|
||||
nuxt3: true,
|
||||
capi: true,
|
||||
preset: true,
|
||||
typescript: true
|
||||
},
|
||||
{
|
||||
label: 'Tooltip',
|
||||
to: '/components/Tooltip',
|
||||
nuxt3: true,
|
||||
capi: true
|
||||
capi: true,
|
||||
typescript: true
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
BIN
docs/public/favicon.ico
Normal file
BIN
docs/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
51
package.json
51
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxthq/ui",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.26",
|
||||
"repository": "https://github.com/nuxtlabs/ui",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
@@ -18,37 +18,44 @@
|
||||
"build": "nuxt-module-build",
|
||||
"prepack": "yarn build",
|
||||
"dev": "nuxi dev docs",
|
||||
"build:docs": "nuxi build docs",
|
||||
"postbuild:docs": "mv ./docs/.vercel_build_output .vercel_build_output",
|
||||
"build:docs": "nuxi generate docs",
|
||||
"lint": "eslint --ext .ts,.js,.vue .",
|
||||
"typecheck": "nuxi typecheck",
|
||||
"prepare": "nuxi prepare docs",
|
||||
"release": "yarn lint && standard-version && git push --follow-tags"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "npm:@nuxt/kit-edge@latest",
|
||||
"@headlessui/vue": "^1.4.3",
|
||||
"@iconify-json/heroicons-outline": "^1.0.2",
|
||||
"@iconify-json/heroicons-solid": "^1.0.2",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@unocss/nuxt": "^0.22.5",
|
||||
"@vueuse/core": "^7.5.4",
|
||||
"defu": "^5.0.1",
|
||||
"gradient-avatar": "^1.0.2",
|
||||
"@headlessui/vue": "1.7.4",
|
||||
"@iconify/vue": "^4.0.2",
|
||||
"@nuxt/kit": "^3.0.0",
|
||||
"@nuxtjs/color-mode": "^3.2.0",
|
||||
"@nuxtjs/tailwindcss": "^6.2.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@vueuse/core": "^9.10.0",
|
||||
"@vueuse/integrations": "^9.10.0",
|
||||
"defu": "^6.1.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^3.2.0",
|
||||
"pathe": "^0.2.0"
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/module-builder": "^0.1.7",
|
||||
"@nuxtjs/eslint-config-typescript": "^8.0.0",
|
||||
"@types/lodash-es": "^4.17.5",
|
||||
"@vueuse/components": "^7.5.4",
|
||||
"eslint": "^8.7.0",
|
||||
"nuxt3": "latest",
|
||||
"standard-version": "^9.3.2"
|
||||
"@nuxt/module-builder": "^0.2.1",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"eslint": "^8.31.0",
|
||||
"nuxt": "^3.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"unbuild": "^1.0.2",
|
||||
"vue-tsc": "^1.0.20"
|
||||
},
|
||||
"build": {
|
||||
"externals": [
|
||||
"@unocss/preset-uno"
|
||||
"tailwindcss",
|
||||
"tailwindcss/colors.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
6
src/env.d.ts
vendored
Normal file
6
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { DefaultPreset } from './runtime/presets/default'
|
||||
|
||||
declare module '#build/ui' {
|
||||
declare const preset: DefaultPreset
|
||||
export default preset
|
||||
}
|
||||
253
src/module.ts
253
src/module.ts
@@ -1,8 +1,21 @@
|
||||
import { resolve } from 'pathe'
|
||||
import { defineNuxtModule, installModule, addComponentsDir, addTemplate, addPlugin, resolveModule } from '@nuxt/kit'
|
||||
import { colors } from '@unocss/preset-uno'
|
||||
import defu from 'defu'
|
||||
import type { UnocssNuxtOptions } from '@unocss/nuxt'
|
||||
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit'
|
||||
import { defu } from 'defu'
|
||||
import colors from 'tailwindcss/colors.js'
|
||||
import type { Config } from 'tailwindcss'
|
||||
import { name, version } from '../package.json'
|
||||
import { colorsAsRegex, excludeColors } from './runtime/utils/colors'
|
||||
import defaultPreset from './runtime/presets/default'
|
||||
|
||||
// @ts-ignore
|
||||
delete colors.lightBlue
|
||||
// @ts-ignore
|
||||
delete colors.warmGray
|
||||
// @ts-ignore
|
||||
delete colors.trueGray
|
||||
// @ts-ignore
|
||||
delete colors.coolGray
|
||||
// @ts-ignore
|
||||
delete colors.blueGray
|
||||
|
||||
interface ColorsOptions {
|
||||
/**
|
||||
@@ -11,16 +24,13 @@ interface ColorsOptions {
|
||||
primary?: string
|
||||
|
||||
/**
|
||||
* @default 'zinc'
|
||||
* @default 'gray'
|
||||
*/
|
||||
gray?: string
|
||||
}
|
||||
|
||||
export interface ModuleOptions {
|
||||
/**
|
||||
* @default 'tailwindui'
|
||||
*/
|
||||
preset?: string | object
|
||||
preset?: object
|
||||
|
||||
/**
|
||||
* @default 'u'
|
||||
@@ -29,160 +39,117 @@ export interface ModuleOptions {
|
||||
|
||||
colors?: ColorsOptions
|
||||
|
||||
unocss?: UnocssNuxtOptions
|
||||
tailwindcss?: Partial<Config>
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
preset: 'tailwindui',
|
||||
preset: {},
|
||||
prefix: 'u',
|
||||
colors: {
|
||||
primary: 'indigo',
|
||||
gray: 'zinc'
|
||||
gray: 'gray'
|
||||
},
|
||||
unocss: {
|
||||
shortcuts: [],
|
||||
rules: [],
|
||||
variants: [],
|
||||
tailwindcss: {
|
||||
theme: {}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
meta: {
|
||||
name: '@nuxthq/ui',
|
||||
name,
|
||||
version,
|
||||
configKey: 'ui',
|
||||
compatibility: {
|
||||
nuxt: '^3.0.0',
|
||||
bridge: true
|
||||
nuxt: '^3.0.0-rc.8'
|
||||
}
|
||||
},
|
||||
defaults,
|
||||
async setup (_options, nuxt) {
|
||||
const { preset, prefix, colors: { primary = 'indigo', gray = 'zinc' } = {} } = _options
|
||||
const { shortcuts = [], rules = [], variants = [], theme = {} } = _options.unocss || {}
|
||||
async setup (options, nuxt) {
|
||||
const { preset = {}, prefix, colors: { primary = 'indigo', gray = 'gray' } = {}, tailwindcss: { theme = {} } = {} } = options
|
||||
|
||||
const options: UnocssNuxtOptions = {
|
||||
theme: {
|
||||
colors: {
|
||||
gray: typeof gray === 'object' ? gray : (colors && colors[gray]),
|
||||
primary: typeof primary === 'object' ? primary : (colors && colors[primary])
|
||||
},
|
||||
...theme
|
||||
},
|
||||
preflight: true,
|
||||
icons: {
|
||||
prefix: ''
|
||||
},
|
||||
shortcuts: {
|
||||
[`${prefix}-bg-white`]: 'bg-white dark:bg-black',
|
||||
[`${prefix}-bg-gray-50`]: 'bg-gray-50 dark:bg-gray-900',
|
||||
[`${prefix}-bg-gray-100`]: 'bg-gray-100 dark:bg-gray-800',
|
||||
[`${prefix}-bg-gray-200`]: 'bg-gray-200 dark:bg-gray-700',
|
||||
[`${prefix}-bg-gray-300`]: 'bg-gray-300 dark:bg-gray-600',
|
||||
[`${prefix}-bg-gray-400`]: 'bg-gray-400 dark:bg-gray-500',
|
||||
[`${prefix}-bg-gray-500`]: 'bg-gray-500 dark:bg-gray-400',
|
||||
[`${prefix}-bg-gray-600`]: 'bg-gray-600 dark:bg-gray-300',
|
||||
[`${prefix}-bg-gray-700`]: 'bg-gray-700 dark:bg-gray-200',
|
||||
[`${prefix}-bg-gray-800`]: 'bg-gray-800 dark:bg-gray-100',
|
||||
[`${prefix}-bg-gray-900`]: 'bg-gray-900 dark:bg-gray-50',
|
||||
[`${prefix}-bg-black`]: 'bg-black dark:bg-white',
|
||||
[`${prefix}-text-white`]: 'text-white dark:text-black',
|
||||
[`${prefix}-text-gray-50`]: 'text-gray-50 dark:text-gray-900',
|
||||
[`${prefix}-text-gray-100`]: 'text-gray-100 dark:text-gray-800',
|
||||
[`${prefix}-text-gray-200`]: 'text-gray-200 dark:text-gray-700',
|
||||
[`${prefix}-text-gray-300`]: 'text-gray-300 dark:text-gray-600',
|
||||
[`${prefix}-text-gray-400`]: 'text-gray-400 dark:text-gray-500',
|
||||
[`${prefix}-text-gray-500`]: 'text-gray-500 dark:text-gray-400',
|
||||
[`${prefix}-text-gray-600`]: 'text-gray-600 dark:text-gray-300',
|
||||
[`${prefix}-text-gray-700`]: 'text-gray-700 dark:text-gray-200',
|
||||
[`${prefix}-text-gray-800`]: 'text-gray-800 dark:text-gray-100',
|
||||
[`${prefix}-text-gray-900`]: 'text-gray-900 dark:text-gray-50',
|
||||
[`${prefix}-text-black`]: 'text-black dark:text-white',
|
||||
[`${prefix}-border-white`]: 'border-white dark:border-black',
|
||||
[`${prefix}-border-gray-100`]: 'border-gray-100 dark:border-gray-900',
|
||||
[`${prefix}-border-gray-200`]: 'border-gray-200 dark:border-gray-800',
|
||||
[`${prefix}-border-gray-300`]: 'border-gray-300 dark:border-gray-700',
|
||||
[`${prefix}-border-gray-400`]: 'border-gray-400 dark:border-gray-600',
|
||||
[`${prefix}-border-gray-500`]: 'border-gray-500 dark:border-gray-500',
|
||||
[`${prefix}-border-gray-600`]: 'border-gray-600 dark:border-gray-400',
|
||||
[`${prefix}-border-gray-700`]: 'border-gray-700 dark:border-gray-300',
|
||||
[`${prefix}-border-gray-800`]: 'border-gray-800 dark:border-gray-200',
|
||||
[`${prefix}-border-gray-900`]: 'border-gray-900 dark:border-gray-100',
|
||||
[`${prefix}-border-black`]: 'border-black dark:border-white',
|
||||
[`${prefix}-divide-white`]: 'divide-white dark:divide-black',
|
||||
[`${prefix}-divide-gray-100`]: 'divide-gray-100 dark:divide-gray-900',
|
||||
[`${prefix}-divide-gray-200`]: 'divide-gray-200 dark:divide-gray-800',
|
||||
[`${prefix}-divide-gray-300`]: 'divide-gray-300 dark:divide-gray-700',
|
||||
[`${prefix}-divide-gray-400`]: 'divide-gray-400 dark:divide-gray-600',
|
||||
[`${prefix}-divide-gray-500`]: 'divide-gray-500 dark:divide-gray-500',
|
||||
[`${prefix}-divide-gray-600`]: 'divide-gray-600 dark:divide-gray-400',
|
||||
[`${prefix}-divide-gray-700`]: 'divide-gray-700 dark:divide-gray-300',
|
||||
[`${prefix}-divide-gray-800`]: 'divide-gray-800 dark:divide-gray-200',
|
||||
[`${prefix}-divide-gray-900`]: 'divide-gray-900 dark:divide-gray-100',
|
||||
[`${prefix}-divide-black`]: 'divide-black dark:divide-white',
|
||||
[`${prefix}-ring-white`]: 'ring-white dark:ring-black',
|
||||
[`${prefix}-ring-gray-100`]: 'ring-gray-100 dark:ring-gray-900',
|
||||
[`${prefix}-ring-gray-200`]: 'ring-gray-200 dark:ring-gray-800',
|
||||
[`${prefix}-ring-gray-300`]: 'ring-gray-300 dark:ring-gray-700',
|
||||
[`${prefix}-ring-gray-400`]: 'ring-gray-400 dark:ring-gray-600',
|
||||
[`${prefix}-ring-gray-500`]: 'ring-gray-500 dark:ring-gray-500',
|
||||
[`${prefix}-ring-gray-600`]: 'ring-gray-600 dark:ring-gray-400',
|
||||
[`${prefix}-ring-gray-700`]: 'ring-gray-700 dark:ring-gray-300',
|
||||
[`${prefix}-ring-gray-800`]: 'ring-gray-800 dark:ring-gray-200',
|
||||
[`${prefix}-ring-gray-900`]: 'ring-gray-900 dark:ring-gray-100',
|
||||
[`${prefix}-ring-black`]: 'ring-black dark:ring-white',
|
||||
...shortcuts
|
||||
},
|
||||
rules: [
|
||||
[/^shadow-?(.*)$/, ([, d], { theme }) => {
|
||||
// @ts-ignore
|
||||
const value = theme?.boxShadow?.[d || 'DEFAULT']
|
||||
if (value) {
|
||||
return {
|
||||
'--un-shadow-color': '0,0,0',
|
||||
'--un-shadow': value,
|
||||
'box-shadow': 'var(--un-shadow)'
|
||||
}
|
||||
}
|
||||
}],
|
||||
...rules
|
||||
],
|
||||
variants,
|
||||
layers: {
|
||||
icons: 0,
|
||||
default: 1,
|
||||
shortcuts: 2
|
||||
}
|
||||
}
|
||||
|
||||
await installModule('@unocss/nuxt', options, nuxt)
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
|
||||
// Transpile runtime
|
||||
const runtimeDir = resolve(__dirname, './runtime')
|
||||
const runtimeDir = resolve('./runtime')
|
||||
nuxt.options.build.transpile.push(runtimeDir)
|
||||
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue')
|
||||
nuxt.options.build.transpile.push('@popperjs/core', '@headlessui/vue', '@iconify/vue')
|
||||
|
||||
const presetsDir = resolve(runtimeDir, './presets')
|
||||
|
||||
let ui: object = (await import(resolveModule(`./${defaults.preset}`, { paths: presetsDir }))).default
|
||||
try {
|
||||
if (typeof preset === 'object') {
|
||||
ui = defu(preset, ui)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
ui = (await import(resolveModule(`./${preset}`, { paths: presetsDir }))).default
|
||||
// @ts-ignore
|
||||
nuxt.hook('tailwindcss:config', function (tailwindConfig: TailwindConfig) {
|
||||
const globalColors = {
|
||||
...(tailwindConfig.theme.colors || colors),
|
||||
...tailwindConfig.theme.extend?.colors
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Could not load preset file.')
|
||||
}
|
||||
|
||||
addTemplate({
|
||||
filename: 'ui.mjs',
|
||||
getContents: () => `/* @unocss-include */ export default ${JSON.stringify(ui)}`
|
||||
// @ts-ignore
|
||||
tailwindConfig.theme.extend.colors = tailwindConfig.theme.extend.colors || {}
|
||||
// @ts-ignore
|
||||
globalColors.primary = tailwindConfig.theme.extend.colors.primary = globalColors[primary] || colors[primary]
|
||||
// @ts-ignore
|
||||
globalColors.gray = tailwindConfig.theme.extend.colors.gray = globalColors[gray] || colors[gray]
|
||||
|
||||
const variantColors = excludeColors(globalColors)
|
||||
const safeColorsAsRegex = colorsAsRegex(variantColors)
|
||||
|
||||
tailwindConfig.safelist = tailwindConfig.safelist || []
|
||||
tailwindConfig.safelist.push(...[{
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`)
|
||||
},
|
||||
{
|
||||
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(100|600|700)`),
|
||||
variants: ['hover', 'disabled', 'dark']
|
||||
},
|
||||
{
|
||||
pattern: new RegExp(`text-(${safeColorsAsRegex})-(100|800)`),
|
||||
variants: ['dark']
|
||||
},
|
||||
{
|
||||
pattern: new RegExp(`ring-(${safeColorsAsRegex})-(500)`),
|
||||
variants: ['focus']
|
||||
}])
|
||||
|
||||
const ui: object = defu(preset, defaultPreset(variantColors))
|
||||
|
||||
addTemplate({
|
||||
filename: 'ui.mjs',
|
||||
getContents: () => `export default ${JSON.stringify(ui)}`
|
||||
})
|
||||
addTemplate({
|
||||
filename: 'ui.d.ts',
|
||||
write: true,
|
||||
getContents: () => 'declare const d: any; export default d;'
|
||||
})
|
||||
})
|
||||
|
||||
await installModule('@nuxtjs/color-mode', { classSuffix: '' })
|
||||
await installModule('@nuxtjs/tailwindcss', {
|
||||
viewer: false,
|
||||
config: {
|
||||
darkMode: 'class',
|
||||
theme,
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
require('@tailwindcss/typography')
|
||||
],
|
||||
content: [
|
||||
resolve(runtimeDir, 'components/**/*.{vue,js,ts}'),
|
||||
resolve(runtimeDir, 'presets/*.{mjs,js,ts}')
|
||||
],
|
||||
safelist: [
|
||||
'dark',
|
||||
{
|
||||
pattern: /rounded-(sm|md|lg|xl|2xl|3xl)/,
|
||||
variants: ['sm']
|
||||
}
|
||||
]
|
||||
},
|
||||
cssPath: resolve(runtimeDir, 'tailwind.css')
|
||||
})
|
||||
|
||||
addPlugin(resolve(runtimeDir, 'plugins', 'toast.client'))
|
||||
addPlugin(resolve(runtimeDir, 'plugins', 'clipboard.client'))
|
||||
|
||||
addComponentsDir({
|
||||
path: resolve(runtimeDir, 'components', 'elements'),
|
||||
@@ -215,18 +182,6 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
watch: false
|
||||
})
|
||||
|
||||
// Add composables
|
||||
nuxt.hook('autoImports:dirs', (dirs) => {
|
||||
dirs.push(resolve(runtimeDir, 'composables'))
|
||||
})
|
||||
|
||||
// Add CSS
|
||||
nuxt.options.css.push(resolve(runtimeDir, 'css', 'forms.css'))
|
||||
|
||||
nuxt.options.vite = {
|
||||
optimizeDeps: {
|
||||
include: ['gradient-avatar']
|
||||
}
|
||||
}
|
||||
addImportsDir(resolve(runtimeDir, 'composables'))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,165 +1,127 @@
|
||||
<template>
|
||||
<span class="relative inline-flex items-center justify-center" :class="avatarClass" @click="goto">
|
||||
<img v-if="url" :src="url" :alt="alt" :class="[sizeClass, roundedClass]">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-else-if="gradientPlaceholder" class="w-full h-full overflow-hidden" :class="roundedClass" v-html="gradientPlaceholder" />
|
||||
<span
|
||||
v-else-if="placeholder"
|
||||
class="font-medium leading-none u-text-gray-900 uppercase"
|
||||
>{{ placeholder }}</span>
|
||||
<span
|
||||
v-else-if="text"
|
||||
>{{ text }}</span>
|
||||
<svg
|
||||
v-else
|
||||
class="w-full h-full u-text-gray-300"
|
||||
:class="roundedClass"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span :class="wrapperClass">
|
||||
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
|
||||
<span v-else-if="text || placeholder" :class="placeholderClass">{{ text || placeholder }}</span>
|
||||
|
||||
<span
|
||||
v-if="status"
|
||||
class="absolute top-0 right-0 block rounded-full ring-1 u-ring-white"
|
||||
:class="statusClass"
|
||||
/>
|
||||
<span v-if="chip" :class="chipClass" />
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import avatar from 'gradient-avatar'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'].includes(value)
|
||||
}
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
gradient: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value) {
|
||||
return ['online', 'idle', 'invisible', 'donotdisturb', 'focus'].includes(value)
|
||||
}
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
url () {
|
||||
if (typeof this.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return this.src
|
||||
},
|
||||
placeholder () {
|
||||
if (!this.alt) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.alt.split(' ').map(word => word.charAt(0)).join('')
|
||||
},
|
||||
gradientPlaceholder () {
|
||||
if (!this.gradient) {
|
||||
return
|
||||
}
|
||||
|
||||
return avatar(this.alt || new Date().toString())
|
||||
},
|
||||
sizeClass () {
|
||||
return ({
|
||||
xxxs: 'h-4 w-4 text-xs',
|
||||
xxs: 'h-5 w-5 text-xs',
|
||||
xs: 'h-6 w-6 text-xs',
|
||||
sm: 'h-8 w-8 text-sm',
|
||||
md: 'h-10 w-10 text-md',
|
||||
lg: 'h-12 w-12 text-lg',
|
||||
xl: 'h-14 w-14 text-xl',
|
||||
'2xl': 'h-16 w-16 text-2xl',
|
||||
'3xl': 'h-20 w-20 text-3xl'
|
||||
})[this.size]
|
||||
},
|
||||
roundedClass () {
|
||||
return ({
|
||||
true: 'rounded-lg',
|
||||
false: 'rounded-full'
|
||||
})[this.rounded]
|
||||
},
|
||||
placeholderClass () {
|
||||
return ({
|
||||
true: 'u-bg-gray-100',
|
||||
false: 'u-bg-gray-100'
|
||||
})[!!this.alt]
|
||||
},
|
||||
avatarClass () {
|
||||
return [
|
||||
this.sizeClass,
|
||||
this.roundedClass,
|
||||
this.placeholderClass,
|
||||
this.to ? 'cursor-pointer' : ''
|
||||
].join(' ')
|
||||
},
|
||||
statusClass () {
|
||||
return [
|
||||
({
|
||||
online: 'bg-green-400',
|
||||
idle: 'bg-yellow-400',
|
||||
invisible: 'u-bg-gray-300',
|
||||
donotdisturb: 'bg-red-400',
|
||||
focus: 'bg-primary-500'
|
||||
})[this.status],
|
||||
({
|
||||
xxxs: 'h-1 w-1',
|
||||
xxs: 'h-1 w-1',
|
||||
xs: 'h-1.5 w-1.5',
|
||||
sm: 'h-2 w-2',
|
||||
md: 'h-2.5 w-2.5',
|
||||
lg: 'h-3 w-3',
|
||||
xl: 'h-3.5 w-3.5',
|
||||
'2xl': 'h-3.5 w-3.5',
|
||||
'3xl': 'h-4 w-4'
|
||||
})[this.size],
|
||||
({
|
||||
true: 'transform -translate-y-1/2 translate-x-1/2'
|
||||
})[this.rounded]
|
||||
].join(' ')
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
chip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
chipVariant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.variant).includes(value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goto (e) {
|
||||
if (!this.to || !this.$router) { return }
|
||||
e.preventDefault()
|
||||
this.$router.push(this.to)
|
||||
chipPosition: {
|
||||
type: String,
|
||||
default: 'top-right',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.position).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.wrapper
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.background
|
||||
},
|
||||
placeholderClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.placeholder
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.rounded
|
||||
}
|
||||
})
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
props.wrapperClass,
|
||||
props.backgroundClass,
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const chipClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.chip.base,
|
||||
$ui.avatar.chip.position[props.chipPosition],
|
||||
$ui.avatar.chip.variant[props.chipVariant],
|
||||
$ui.avatar.chip.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const url = computed(() => {
|
||||
if (typeof props.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
|
||||
})
|
||||
|
||||
const error = ref(false)
|
||||
|
||||
watch(() => props.src, () => {
|
||||
if (error.value) {
|
||||
error.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function onError () {
|
||||
error.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatar' }
|
||||
</script>
|
||||
|
||||
@@ -1,62 +1,79 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<Avatar
|
||||
v-for="(avatar, index) of avatars"
|
||||
:key="index"
|
||||
:src="avatar.src"
|
||||
class="ring-2 u-ring-white -ml-1.5 first:ml-0"
|
||||
:size="size"
|
||||
:status="avatar.status"
|
||||
/>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Avatar
|
||||
v-if="remainingGroupSize > 0"
|
||||
class="ring-2 u-ring-white -ml-1.5 first:ml-0 text-[10px]"
|
||||
:size="size"
|
||||
:text="`+${remainingGroupSize}`"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
<Avatar
|
||||
v-for="(avatar, index) of displayedGroup"
|
||||
:key="index"
|
||||
v-bind="avatar"
|
||||
:size="size"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from './Avatar'
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatars () {
|
||||
return this.group.map((avatar) => {
|
||||
return typeof avatar === 'string' ? { src: avatar } : avatar
|
||||
})
|
||||
},
|
||||
displayedGroup () {
|
||||
if (!this.max) { return this.avatars }
|
||||
|
||||
return this.avatars.slice(0, this.max)
|
||||
},
|
||||
remainingGroupSize () {
|
||||
if (!this.max) { return 0 }
|
||||
|
||||
return this.avatars.length - this.max
|
||||
}
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.ring
|
||||
},
|
||||
marginClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.margin
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const avatars = computed(() => {
|
||||
return props.group.map((avatar) => {
|
||||
return typeof avatar === 'string' ? { src: avatar } : avatar
|
||||
})
|
||||
})
|
||||
|
||||
const displayedGroup = computed(() => {
|
||||
if (!props.max) { return [...avatars.value].reverse() }
|
||||
|
||||
return avatars.value.slice(0, props.max).reverse()
|
||||
})
|
||||
|
||||
const remainingGroupSize = computed(() => {
|
||||
if (!props.max) { return 0 }
|
||||
|
||||
return avatars.value.length - props.max
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
props.ringClass,
|
||||
props.marginClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatarGroup' }
|
||||
</script>
|
||||
|
||||
@@ -4,53 +4,50 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return Object.keys($ui.badge.size).includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value) {
|
||||
return Object.keys($ui.badge.variant).includes(value)
|
||||
}
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.badge.base
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.size).includes(value)
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.badge.size[props.size],
|
||||
$ui.badge.variant[props.variant],
|
||||
props.rounded ? 'rounded-full' : 'rounded-md'
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
badgeClass
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.variant).includes(value)
|
||||
}
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.badge.base
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.badge.size[props.size],
|
||||
$ui.badge.variant[props.variant],
|
||||
props.rounded ? 'rounded-full' : 'rounded-md'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UBadge' }
|
||||
</script>
|
||||
|
||||
@@ -6,180 +6,209 @@
|
||||
:aria-label="ariaLabel"
|
||||
v-bind="buttonProps"
|
||||
>
|
||||
<Icon v-if="isLeading" :name="iconName" :class="iconClass" aria-hidden="true" />
|
||||
<Icon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||
<slot><span :class="truncate ? 'text-left break-all line-clamp-1' : ''">{{ label }}</span></slot>
|
||||
<Icon v-if="isTrailing" :name="iconName" :class="iconClass" aria-hidden="true" />
|
||||
<Icon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
import Link from '../elements/Link'
|
||||
import Icon from '../elements/Icon'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useSlots } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
Link
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return Object.keys($ui.button.size).includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value) {
|
||||
return Object.keys($ui.button.variant).includes(value)
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.size).includes(value)
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
const button = ref(null)
|
||||
|
||||
const buttonIs = computed(() => {
|
||||
if (props.to) {
|
||||
return 'Link'
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
|
||||
const buttonProps = computed(() => {
|
||||
switch (buttonIs.value) {
|
||||
case 'Link': return { to: props.to, target: props.target }
|
||||
default: return { disabled: props.disabled || props.loading, type: props.type }
|
||||
}
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
||||
})
|
||||
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.button.size[props.size],
|
||||
$ui.button[isSquare.value ? 'square' : 'spacing'][props.size],
|
||||
$ui.button.variant[props.variant],
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
|
||||
props.rounded ? 'rounded-full' : 'rounded-md',
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.input.icon.size[props.size],
|
||||
isLeading.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.leading.spacing[props.size],
|
||||
isTrailing.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.trailing.spacing[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
button,
|
||||
buttonIs,
|
||||
buttonProps,
|
||||
buttonClass,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
iconName,
|
||||
iconClass
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.variant).includes(value)
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.rounded
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.base
|
||||
},
|
||||
leadingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
trailingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const button = ref(null)
|
||||
|
||||
const buttonIs = computed(() => {
|
||||
if (props.to) {
|
||||
return NuxtLink
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
|
||||
const buttonProps = computed(() => {
|
||||
if (props.to) {
|
||||
return { to: props.to, target: props.target }
|
||||
} else {
|
||||
return { disabled: props.disabled || props.loading, type: props.type }
|
||||
}
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.button.size[props.size],
|
||||
$ui.button[isSquare.value ? 'square' : 'spacing'][props.size],
|
||||
$ui.button.variant[props.variant],
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
|
||||
props.rounded ? 'rounded-full' : props.roundedClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.leading.spacing[props.size],
|
||||
props.leadingIconClass,
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.trailing.spacing[props.size],
|
||||
props.trailingIconClass,
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UButton' }
|
||||
</script>
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
<template>
|
||||
<Menu v-slot="{ open }" as="div" :class="wrapperClass">
|
||||
<MenuButton ref="trigger" as="div">
|
||||
<Menu v-slot="{ open }" as="div" :class="wrapperClass" @mouseleave="onMouseLeave">
|
||||
<MenuButton ref="trigger" as="div" class="inline-flex w-full" @mouseover="onMouseOver">
|
||||
<slot :open="open">
|
||||
<button>Open</button>
|
||||
</slot>
|
||||
</MenuButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="containerClass">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-out"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<MenuItems :class="itemsClass" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" class="py-1">
|
||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }">
|
||||
<Component v-bind="item" :is="(item.to && 'Link') || 'button'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)">
|
||||
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<MenuItems :class="baseClass" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="groupClass">
|
||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }" :disabled="item.disabled">
|
||||
<Component
|
||||
v-bind="omit(item, ['click'])"
|
||||
:is="(item.to && NuxtLink) || (item.click && 'button') || 'div'"
|
||||
:class="resolveItemClass({ active, disabled })"
|
||||
@click="item.click"
|
||||
>
|
||||
<slot :name="item.slot" :item="item">
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" />
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="[itemIconClass, item.iconClass]" />
|
||||
<Avatar v-if="item.avatar" v-bind="{ size: 'xxs', ...item.avatar }" :class="itemAvatarClass" />
|
||||
|
||||
{{ item.label }}
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
|
||||
<span v-if="item.shortcuts?.length" :class="itemShortcutsClass">
|
||||
<kbd v-for="shortcut of item.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
||||
</span>
|
||||
</slot>
|
||||
</Component>
|
||||
</MenuItem>
|
||||
@@ -34,128 +36,186 @@
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem
|
||||
} from '@headlessui/vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import { classNames, omit } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
import Icon from '../elements/Icon'
|
||||
import Link from '../elements/Link'
|
||||
import { classNames, usePopper } from '../../utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Link
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as PropType<{
|
||||
to?: RouteLocationNormalized
|
||||
exact?: boolean
|
||||
label: string
|
||||
disabled?: boolean
|
||||
slot?: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
avatar?: Partial<AvatarType>
|
||||
click?: Function
|
||||
shortcuts?: string[]
|
||||
}[][]>,
|
||||
default: () => []
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-end',
|
||||
validator: (value) => {
|
||||
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
|
||||
}
|
||||
},
|
||||
strategy: {
|
||||
type: String,
|
||||
default: 'fixed',
|
||||
validator: (value) => {
|
||||
return ['absolute', 'fixed'].includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: 'relative inline-block text-left'
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: 'w-48 z-20'
|
||||
},
|
||||
itemsClass: {
|
||||
type: String,
|
||||
default: 'u-bg-white divide-y u-divide-gray-100 rounded-md ring-1 ring-black ring-opacity-5'
|
||||
},
|
||||
itemClass: {
|
||||
type: String,
|
||||
default: 'group flex items-center px-4 py-2 text-sm w-full'
|
||||
},
|
||||
itemActiveClass: {
|
||||
type: String,
|
||||
default: 'u-bg-gray-100 u-text-gray-900'
|
||||
},
|
||||
itemInactiveClass: {
|
||||
type: String,
|
||||
default: 'u-text-gray-700'
|
||||
},
|
||||
itemDisabledClass: {
|
||||
type: String,
|
||||
default: 'cursor-not-allowed opacity-50'
|
||||
},
|
||||
itemIconClass: {
|
||||
type: String,
|
||||
default: 'mr-3 h-5 w-5 u-text-gray-400 group-hover:u-text-gray-500'
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const [trigger, container] = usePopper({
|
||||
placement: props.placement,
|
||||
strategy: props.strategy,
|
||||
modifiers: [{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false,
|
||||
adaptive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: 8
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
function resolveItemClass ({ active, disabled }) {
|
||||
return classNames(
|
||||
props.itemClass,
|
||||
active ? props.itemActiveClass : props.itemInactiveClass,
|
||||
disabled && props.itemDisabledClass
|
||||
)
|
||||
}
|
||||
|
||||
function onItemClick (item) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.click) {
|
||||
item.click()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
trigger,
|
||||
container,
|
||||
onItemClick,
|
||||
resolveItemClass
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.width
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.dropdown.transition
|
||||
},
|
||||
groupClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.group
|
||||
},
|
||||
itemBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.base
|
||||
},
|
||||
itemActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.active
|
||||
},
|
||||
itemInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.inactive
|
||||
},
|
||||
itemDisabledClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.disabled
|
||||
},
|
||||
itemIconClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.icon
|
||||
},
|
||||
itemAvatarClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.avatar
|
||||
},
|
||||
itemShortcutsClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.shortcuts
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.dropdown.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
|
||||
return classNames(
|
||||
props.itemBaseClass,
|
||||
active ? props.itemActiveClass : props.itemInactiveClass,
|
||||
disabled && props.itemDisabledClass
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/menu/menu.ts#L131
|
||||
const menuApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
if (props.mode !== 'hover' || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (menuApi.value.menuState === 0) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
menuApi.value.openMenu && menuApi.value.openMenu()
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
if (props.mode !== 'hover' || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (menuApi.value.menuState === 1) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
menuApi.value.closeMenu && menuApi.value.closeMenu()
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UDropdown' }
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,56 @@
|
||||
<template>
|
||||
<div :class="name" />
|
||||
<span v-if="isFetching" />
|
||||
<Iconify v-else-if="icon" :icon="icon" />
|
||||
<Component :is="component" v-else-if="component" />
|
||||
<span v-else>{{ name }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import type { IconifyIcon } from '@iconify/vue'
|
||||
import { Icon as Iconify } from '@iconify/vue/dist/offline'
|
||||
import { loadIcon } from '@iconify/vue'
|
||||
import { useNuxtApp, useState, computed, watch, onMounted } from '#imports'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const nuxtApp = useNuxtApp()
|
||||
const state = useState<Record<string, Required<IconifyIcon> | Promise<void> | null>>('u-icons', () => ({}))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
const isFetching = computed(() => state.value?.[props.name] && ('then' in state.value?.[props.name]!))
|
||||
const icon = computed<Required<IconifyIcon> | null>(() =>
|
||||
!state.value?.[props.name] || 'then' in state.value[props.name]!
|
||||
? null
|
||||
: state.value[props.name] as Required<IconifyIcon>
|
||||
)
|
||||
const component = computed(() => nuxtApp.vueApp.component(props.name))
|
||||
|
||||
const loadIconComponent = (name: string) => {
|
||||
state.value = state.value || {}
|
||||
if (nuxtApp.vueApp.component(props.name) || state.value[name] || state.value[name] === null) { return state.value[name] }
|
||||
|
||||
state.value[name] = loadIcon(name)
|
||||
.then((res) => { state.value[name] = res })
|
||||
// We won't try to load this icon again if it fails
|
||||
.catch(() => { state.value[name] = null })
|
||||
|
||||
// return an awaitable promise
|
||||
return state.value[name]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch(() => props.name, loadIconComponent, { immediate: true })
|
||||
})
|
||||
|
||||
if (process.server) {
|
||||
await loadIconComponent(props.name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UIcon' }
|
||||
</script>
|
||||
|
||||
@@ -1,79 +1,73 @@
|
||||
<template>
|
||||
<button v-if="isButton" v-bind="$attrs" :class="isActive ? activeClass : inactiveClass">
|
||||
<slot v-bind="{ isActive }" />
|
||||
<button v-if="isButton" v-bind="$attrs" :class="inactiveClass">
|
||||
<slot />
|
||||
</button>
|
||||
<a v-else-if="isExternalLink" v-bind="$attrs" :class="isActive ? activeClass : inactiveClass" :href="to" :target="target">
|
||||
<slot v-bind="{ isActive }" />
|
||||
<a v-else-if="isExternalLink" v-bind="$attrs" :href="to" :target="target" :class="inactiveClass">
|
||||
<slot />
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
v-slot="{ href, navigate }"
|
||||
v-bind="$props"
|
||||
v-slot="{ href, navigate, isActive, isExactActive }"
|
||||
v-bind="$props as RouterLinkProps"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="isActive ? activeClass : inactiveClass"
|
||||
:class="resolveLinkClass({ isActive, isExactActive })"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive }" />
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouterLinkProps } from 'vue-router'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useRoute } from '#imports'
|
||||
|
||||
export default {
|
||||
name: 'Link',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...RouterLink.props,
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
const props = defineProps({
|
||||
// @ts-expect-error internal props
|
||||
...RouterLink.props,
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized>,
|
||||
default: null
|
||||
},
|
||||
setup (props) {
|
||||
const route = useRoute()
|
||||
const isActive = computed(() => {
|
||||
if (!props.to) {
|
||||
return false
|
||||
}
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
if (props.exact) {
|
||||
return [props.to, `${props.to}/`].includes(route.path)
|
||||
} else {
|
||||
return !!route.path.startsWith(props.to)
|
||||
}
|
||||
})
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
const isButton = computed(() => {
|
||||
return !props.to
|
||||
})
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
const isButton = computed(() => {
|
||||
return !props.to
|
||||
})
|
||||
|
||||
return {
|
||||
isActive,
|
||||
isButton,
|
||||
isExternalLink
|
||||
}
|
||||
function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
|
||||
if (props.exact) {
|
||||
return isExactActive ? props.activeClass : props.inactiveClass
|
||||
} else {
|
||||
return isActive ? props.activeClass : props.inactiveClass
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ULink',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,102 +2,105 @@
|
||||
<div class="rounded-md p-4" :class="variantClass">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<Icon :name="iconName" :class="iconClass" class="h-5 w-5" />
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" class="h-5 w-5" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p v-if="title" class="text-sm leading-5" :class="titleClass">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p v-if="link" class="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
|
||||
<Link
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
class="whitespace-nowrap font-medium"
|
||||
:class="linkClass"
|
||||
@click="click && click()"
|
||||
>
|
||||
{{ link }} →
|
||||
</Link>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '../elements/Icon'
|
||||
import Link from '../elements/Link'
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
Link
|
||||
},
|
||||
props: {
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
validator (value) {
|
||||
return ['info', 'warning', 'error', 'success'].includes(value)
|
||||
}
|
||||
},
|
||||
to: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
click: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: null
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
validator (value: string) {
|
||||
return ['info', 'warning', 'error', 'success'].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName () {
|
||||
return ({
|
||||
info: 'heroicons-solid:information-circle',
|
||||
warning: 'heroicons-solid:exclamation',
|
||||
error: 'heroicons-solid:x-circle',
|
||||
success: 'heroicons-solid:check-circle'
|
||||
})[this.variant]
|
||||
},
|
||||
variantClass () {
|
||||
return ({
|
||||
info: 'bg-blue-50',
|
||||
warning: 'bg-orange-50',
|
||||
error: 'bg-red-50',
|
||||
success: 'bg-green-50'
|
||||
})[this.variant]
|
||||
},
|
||||
iconClass () {
|
||||
return ({
|
||||
info: 'text-blue-400',
|
||||
warning: 'text-orange-400',
|
||||
error: 'text-red-400',
|
||||
success: 'text-green-400'
|
||||
})[this.variant]
|
||||
},
|
||||
titleClass () {
|
||||
return ({
|
||||
info: 'text-blue-700',
|
||||
warning: 'text-orange-700',
|
||||
error: 'text-red-700',
|
||||
success: 'text-green-700'
|
||||
})[this.variant]
|
||||
},
|
||||
linkClass () {
|
||||
return ({
|
||||
info: 'text-blue-700 hover:text-blue-600',
|
||||
warning: 'text-orange-700 hover:text-orange-600',
|
||||
error: 'text-red-700 hover:text-red-600',
|
||||
success: 'text-green-700 hover:text-green-600'
|
||||
})[this.variant]
|
||||
}
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized>,
|
||||
default: null
|
||||
},
|
||||
click: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
return ({
|
||||
info: 'heroicons-solid:information-circle',
|
||||
warning: 'heroicons-solid:exclamation',
|
||||
error: 'heroicons-solid:x-circle',
|
||||
success: 'heroicons-solid:check-circle'
|
||||
})[props.variant]
|
||||
})
|
||||
|
||||
const variantClass = computed(() => {
|
||||
return ({
|
||||
info: 'bg-blue-50',
|
||||
warning: 'bg-orange-50',
|
||||
error: 'bg-red-50',
|
||||
success: 'bg-green-50'
|
||||
})[props.variant]
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return ({
|
||||
info: 'text-blue-400',
|
||||
warning: 'text-orange-400',
|
||||
error: 'text-red-400',
|
||||
success: 'text-green-400'
|
||||
})[props.variant]
|
||||
})
|
||||
|
||||
const titleClass = computed(() => {
|
||||
return ({
|
||||
info: 'text-blue-700',
|
||||
warning: 'text-orange-700',
|
||||
error: 'text-red-700',
|
||||
success: 'text-green-700'
|
||||
})[props.variant]
|
||||
})
|
||||
|
||||
const linkClass = computed(() => {
|
||||
return ({
|
||||
info: 'text-blue-700 hover:text-blue-600',
|
||||
warning: 'text-orange-700 hover:text-orange-600',
|
||||
error: 'text-red-700 hover:text-red-600',
|
||||
success: 'text-green-700 hover:text-green-600'
|
||||
})[props.variant]
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAlert' }
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal v-model="isOpen" class="w-full">
|
||||
<Modal v-model="isOpen" :appear="appear" class="w-full" @close="onClose">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div v-if="icon" :class="iconWrapperClass" class="mx-auto flex-shrink-0 flex items-center justify-center rounded-full sm:mx-0">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
@@ -20,12 +20,12 @@
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { DialogTitle, DialogDescription } from '@headlessui/vue'
|
||||
import Modal from '../overlays/Modal'
|
||||
import Button from '../elements/Button'
|
||||
import Icon from '../elements/Icon'
|
||||
import Modal from '../overlays/Modal.vue'
|
||||
import Button from '../elements/Button.vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -33,6 +33,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -75,7 +79,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel', 'close'])
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
@@ -94,4 +98,11 @@ function onCancel () {
|
||||
emit('cancel')
|
||||
isOpen.value = false
|
||||
}
|
||||
function onClose () {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAlertDialog' }
|
||||
</script>
|
||||
|
||||
@@ -26,88 +26,85 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Array],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.base
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.label
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.required
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.help
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
isChecked,
|
||||
inputClass
|
||||
}
|
||||
modelValue: {
|
||||
type: [Boolean, Array],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.base
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.label
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.required
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.help
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCheckbox' }
|
||||
</script>
|
||||
|
||||
@@ -21,67 +21,69 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.container
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.label
|
||||
},
|
||||
labelWrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.labelWrapper
|
||||
},
|
||||
descriptionClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.description
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.required
|
||||
},
|
||||
hintClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.hint
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.help
|
||||
}
|
||||
defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.container
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.label
|
||||
},
|
||||
labelWrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.labelWrapper
|
||||
},
|
||||
descriptionClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.description
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.required
|
||||
},
|
||||
hintClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.hint
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.help
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UFormGroup' }
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
||||
<Icon :name="iconName" :class="iconClass" />
|
||||
</div>
|
||||
<input
|
||||
:id="name"
|
||||
ref="input"
|
||||
@@ -16,191 +13,181 @@
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:class="inputClass"
|
||||
@input="onInput($event.target.value)"
|
||||
@input="onInput(($event.target as any).value)"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
<slot />
|
||||
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||
</div>
|
||||
<div v-if="isTrailing" :class="iconTrailingWrapperClass">
|
||||
<Icon :name="iconName" :class="iconClass" />
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Icon from '../elements/Icon'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
spellcheck: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.input.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return Object.keys($ui.input.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value) {
|
||||
return Object.keys($ui.input.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.input.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.input.size).includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const input = ref(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
input.value.focus()
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.input.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const onInput = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, 100)
|
||||
})
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.input.size[props.size],
|
||||
$ui.input.spacing[props.size],
|
||||
$ui.input.appearance[props.appearance],
|
||||
isLeading.value && $ui.input.leading.spacing[props.size],
|
||||
isTrailing.value && $ui.input.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.input.icon.size[props.size],
|
||||
isLeading.value && $ui.input.icon.leading.spacing[props.size],
|
||||
isTrailing.value && $ui.input.icon.trailing.spacing[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const iconLeadingWrapperClass = $ui.input.icon.leading.wrapper
|
||||
const iconTrailingWrapperClass = $ui.input.icon.trailing.wrapper
|
||||
|
||||
return {
|
||||
input,
|
||||
onInput,
|
||||
inputClass,
|
||||
iconName,
|
||||
iconClass,
|
||||
iconLeadingWrapperClass,
|
||||
iconTrailingWrapperClass,
|
||||
isLeading,
|
||||
isTrailing
|
||||
}
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
input.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.input.size[props.size],
|
||||
$ui.input.spacing[props.size],
|
||||
$ui.input.appearance[props.appearance],
|
||||
isLeading.value && $ui.input.leading.spacing[props.size],
|
||||
isTrailing.value && $ui.input.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.input.icon.size[props.size],
|
||||
isLeading.value && $ui.input.icon.leading.spacing[props.size],
|
||||
isTrailing.value && $ui.input.icon.trailing.spacing[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const iconLeadingWrapperClass = $ui.input.icon.leading.wrapper
|
||||
const iconTrailingWrapperClass = $ui.input.icon.trailing.wrapper
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UInput' }
|
||||
</script>
|
||||
|
||||
@@ -26,88 +26,85 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.base
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.label
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.required
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.help
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const radioClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
isChecked,
|
||||
radioClass
|
||||
}
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.base
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.label
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.required
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.help
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const radioClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'URadio' }
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div v-if="icon" :class="iconWrapperClass">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</div>
|
||||
|
||||
<select
|
||||
:id="name"
|
||||
:name="name"
|
||||
:value="modelValue"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:class="selectClass"
|
||||
@input="onInput($event.target.value)"
|
||||
@input="onInput(($event.target as any).value)"
|
||||
>
|
||||
<template v-for="(option, index) in normalizedOptionsWithPlaceholder">
|
||||
<optgroup
|
||||
@@ -24,6 +21,7 @@
|
||||
:key="`${childOption[valueAttribute]}-${index}-${index2}`"
|
||||
:value="childOption[valueAttribute]"
|
||||
:selected="childOption[valueAttribute] === normalizedValue"
|
||||
:disabled="childOption.disabled"
|
||||
v-text="childOption[textAttribute]"
|
||||
/>
|
||||
</optgroup>
|
||||
@@ -32,178 +30,175 @@
|
||||
:key="`${option[valueAttribute]}-${index}`"
|
||||
:value="option[valueAttribute]"
|
||||
:selected="option[valueAttribute] === normalizedValue"
|
||||
:disabled="option.disabled"
|
||||
v-text="option[textAttribute]"
|
||||
/>
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<div v-if="icon" :class="iconWrapperClass">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { get } from 'lodash-es'
|
||||
import Icon from '../elements/Icon'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: ''
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return Object.keys($ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
const select = ref(null)
|
||||
|
||||
const onInput = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const guessOptionValue = (option) => {
|
||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const guessOptionText = (option) => {
|
||||
return get(option, props.textAttribute, get(option, props.valueAttribute))
|
||||
}
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const normalizeOption = (option) => {
|
||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||
return {
|
||||
[props.valueAttribute]: option,
|
||||
[props.textAttribute]: option
|
||||
}
|
||||
}
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||
}
|
||||
|
||||
return {
|
||||
...option,
|
||||
[props.valueAttribute]: guessOptionValue(option),
|
||||
[props.textAttribute]: guessOptionText(option)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedOptions = computed(() => {
|
||||
return props.options.map(option => normalizeOption(option))
|
||||
})
|
||||
|
||||
const normalizedOptionsWithPlaceholder = computed(() => {
|
||||
if (!props.placeholder) {
|
||||
return normalizedOptions.value
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
[props.valueAttribute]: null,
|
||||
[props.textAttribute]: props.placeholder
|
||||
},
|
||||
...normalizedOptions.value
|
||||
]
|
||||
})
|
||||
|
||||
const normalizedValue = computed(() => {
|
||||
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option.value === props.modelValue)
|
||||
if (!foundOption) {
|
||||
return null
|
||||
}
|
||||
|
||||
return foundOption.value
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.select.size[props.size],
|
||||
$ui.select.spacing[props.size],
|
||||
$ui.select.appearance.default,
|
||||
!!props.icon && $ui.select.leading.spacing[props.size],
|
||||
$ui.select.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.select.icon.size[props.size],
|
||||
!!props.icon && $ui.select.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const iconWrapperClass = $ui.select.icon.leading.base
|
||||
const guessOptionText = (option: any) => {
|
||||
return get(option, props.textAttribute, get(option, props.valueAttribute))
|
||||
}
|
||||
|
||||
const normalizeOption = (option: any) => {
|
||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||
return {
|
||||
select,
|
||||
onInput,
|
||||
guessOptionValue,
|
||||
guessOptionText,
|
||||
normalizeOption,
|
||||
normalizedOptions,
|
||||
normalizedOptionsWithPlaceholder,
|
||||
normalizedValue,
|
||||
selectClass,
|
||||
iconClass,
|
||||
iconWrapperClass
|
||||
[props.valueAttribute]: option,
|
||||
[props.textAttribute]: option
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...option,
|
||||
[props.valueAttribute]: guessOptionValue(option),
|
||||
[props.textAttribute]: guessOptionText(option)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedOptions = computed(() => {
|
||||
return props.options.map(option => normalizeOption(option))
|
||||
})
|
||||
|
||||
const normalizedOptionsWithPlaceholder = computed(() => {
|
||||
if (!props.placeholder) {
|
||||
return normalizedOptions.value
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
[props.valueAttribute]: '',
|
||||
[props.textAttribute]: props.placeholder,
|
||||
disabled: true
|
||||
},
|
||||
...normalizedOptions.value
|
||||
]
|
||||
})
|
||||
|
||||
const normalizedValue = computed(() => {
|
||||
const normalizeModelValue = normalizeOption(props.modelValue)
|
||||
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option[props.valueAttribute] === normalizeModelValue[props.valueAttribute])
|
||||
if (!foundOption) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return foundOption[props.valueAttribute]
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.select.size[props.size],
|
||||
$ui.select.spacing[props.size],
|
||||
$ui.select.appearance[props.appearance],
|
||||
!!props.icon && $ui.select.leading.spacing[props.size],
|
||||
$ui.select.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.select.icon.size[props.size],
|
||||
!!props.icon && $ui.select.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const iconWrapperClass = $ui.select.icon.leading.wrapper
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USelect' }
|
||||
</script>
|
||||
|
||||
@@ -1,448 +1,346 @@
|
||||
<template>
|
||||
<div ref="container">
|
||||
<input :value="value" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default">
|
||||
<Combobox
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:nullable="nullable"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="wrapperClass"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1">
|
||||
|
||||
<slot :toggle="toggle" :open="open">
|
||||
<TwButton
|
||||
icon="solid/selector"
|
||||
icon-class="u-text-gray-400"
|
||||
trailing
|
||||
:size="size"
|
||||
:variant="variant"
|
||||
base-class="w-full cursor-default focus:outline-none disabled:cursor-not-allowed disabled:opacity-75"
|
||||
:disabled="disabled || !options || !options.length"
|
||||
@click.native="!disabled && options && options.length && toggle()"
|
||||
>
|
||||
<div v-if="selectedOptions && selectedOptions.length" class="inline-flex w-full px-3 py-2 -my-2 -ml-3 truncate">
|
||||
<span v-for="(selectedOption, index) of selectedOptions" :key="index" class="inline-flex items-center pr-2">
|
||||
<slot name="label" :option="selectedOption">
|
||||
<span class="u-text-gray-700">{{ selectedOption[textAttribute] }}</span>
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="inline-flex w-full u-text-gray-400">
|
||||
{{ placeholder || '' }}
|
||||
</div>
|
||||
</TwButton>
|
||||
</slot>
|
||||
|
||||
<transition
|
||||
enter-class=""
|
||||
enter-active-class=""
|
||||
enter-to-class=""
|
||||
leave-class="opacity-100"
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-show="open" ref="tooltip" class="z-10 overflow-hidden bg-white rounded-md shadow-lg dark:bg-gray-800 ring-1 u-ring-gray-200" :class="dropdownClass">
|
||||
<div v-if="searchable" class="w-full border-b u-border-gray-200">
|
||||
<TwInput
|
||||
ref="search"
|
||||
v-model="q"
|
||||
type="search"
|
||||
:name="`select-search-${name}`"
|
||||
block
|
||||
autocomplete="off"
|
||||
appearance="none"
|
||||
:placeholder="placeholderSearch"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
ref="options"
|
||||
tabindex="-1"
|
||||
role="listbox"
|
||||
class="overflow-y-auto max-h-60 sm:text-sm focus:outline-none"
|
||||
>
|
||||
<li
|
||||
v-if="showNewOption"
|
||||
ref="option-new"
|
||||
role="option"
|
||||
class="relative pl-3 pr-12 cursor-default select-none group hover:text-white hover:bg-primary-600"
|
||||
:class="{
|
||||
'bg-primary-600 text-white': active === -1,
|
||||
'u-text-gray-900': active !== -1,
|
||||
'py-2': dropdownSize === 'md',
|
||||
'py-1 text-sm': dropdownSize === 'sm'
|
||||
}"
|
||||
@mouseover="active = -1"
|
||||
@click="active === -1 && newOption()"
|
||||
>
|
||||
<slot name="newOption" :optionName="q">
|
||||
<span class="block truncate">Add new option: "{{ q }}"</span>
|
||||
</slot>
|
||||
</li>
|
||||
<li
|
||||
v-for="(option, index) in filteredNormalizedOptions"
|
||||
:key="index"
|
||||
:ref="`option-${index}`"
|
||||
role="option"
|
||||
class="relative pl-3 pr-12 cursor-default select-none group hover:text-white hover:bg-primary-600"
|
||||
:class="{
|
||||
'font-semibold': isOptionSelected(option),
|
||||
'bg-primary-600 text-white': active === index,
|
||||
'u-text-gray-900': active !== index,
|
||||
'py-2': dropdownSize === 'md',
|
||||
'py-1 text-sm': dropdownSize === 'sm'
|
||||
}"
|
||||
@mouseover="active = index"
|
||||
@click.prevent="active === index && selectOption(option)"
|
||||
>
|
||||
<slot name="option" :option="option">
|
||||
<span class="block truncate">{{ option[textAttribute] }}</span>
|
||||
</slot>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<Icon
|
||||
v-if="isOptionSelected(option)"
|
||||
name="solid/check"
|
||||
class=" group-hover:text-white"
|
||||
:class="{
|
||||
'text-white': active === index,
|
||||
'text-primary-600': active !== index,
|
||||
'h-5 w-5': dropdownSize === 'md',
|
||||
'h-4 w-4': dropdownSize === 'sm'
|
||||
}"
|
||||
/>
|
||||
<ComboboxButton ref="trigger" v-slot="{ disabled: buttonDisabled }" as="div" class="inline-flex w-full">
|
||||
<slot :open="open" :disabled="buttonDisabled">
|
||||
<button :class="selectCustomClass" :disabled="disabled" type="button">
|
||||
<slot name="label">
|
||||
<span v-if="modelValue" class="block truncate">{{ (modelValue as any)[textAttribute] }}</span>
|
||||
<span v-else class="block truncate u-text-gray-400">{{ placeholder }}</span>
|
||||
</slot>
|
||||
<slot name="icon">
|
||||
<span :class="iconWrapperClass">
|
||||
<Icon v-if="icon" :name="icon" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</slot>
|
||||
</button>
|
||||
</slot>
|
||||
</ComboboxButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="[listContainerClass, listWidthClass]">
|
||||
<transition appear v-bind="listTransitionClass">
|
||||
<ComboboxOptions static :class="listBaseClass">
|
||||
<ComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
:display-value="() => query"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="listInputClass"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
<ComboboxOption
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="resolveOptionClass({ active, selected, disabled: optionDisabled })">
|
||||
<div :class="listOptionContainerClass">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<span class="block truncate">{{ option[textAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="resolveOptionIconClass({ active })">
|
||||
<Icon v-if="listOptionIcon" :name="listOptionIcon" :class="listOptionIconSizeClass" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
|
||||
<ComboboxOption v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<li :class="resolveOptionClass({ active, selected })">
|
||||
<div :class="listOptionContainerClass">
|
||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
||||
<span class="block truncate">Create "{{ queryOption[textAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="listOptionEmptyClass">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results found for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
</ComboboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from 'lodash-es'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
// import { directive as onClickaway } from 'vue-clickaway'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { PropType, ComponentPublicInstance } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
ComboboxInput
|
||||
} from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
import Icon from '../elements/Icon'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
// directives: {
|
||||
// onClickaway
|
||||
// },
|
||||
shortcuts: {
|
||||
disabled () {
|
||||
return !this.open
|
||||
},
|
||||
up: 'prev',
|
||||
down: 'next',
|
||||
enter: 'enter',
|
||||
esc: {
|
||||
handler: 'close',
|
||||
stop: true,
|
||||
prevent: true
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[]>,
|
||||
default: () => []
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Select an option'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.selectCustom.size).includes(value)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
placeholderSearch: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
newEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
||||
}
|
||||
},
|
||||
dropdownClass: {
|
||||
type: String,
|
||||
default: 'w-full'
|
||||
},
|
||||
dropdownSize: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return ['sm', 'md'].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'gray'
|
||||
},
|
||||
strategy: {
|
||||
type: String,
|
||||
default: 'absolute'
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-start'
|
||||
},
|
||||
unselectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.base
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.icon.name
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.selectCustom.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
open: false,
|
||||
active: 0,
|
||||
q: '',
|
||||
instance: null
|
||||
}
|
||||
listBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.base
|
||||
},
|
||||
computed: {
|
||||
showNewOption () {
|
||||
return this.newEnabled && this.q && !this.filteredNormalizedOptions.find(option => option[this.textAttribute].toLowerCase() === this.q.toLowerCase())
|
||||
},
|
||||
selectedOptions () {
|
||||
if (this.multiple) {
|
||||
return this.value.map(value => this.normalizedOptions.find(option => option[this.valueAttribute] === value)).filter(Boolean)
|
||||
} else {
|
||||
return [this.normalizedOptions.find(option => option[this.valueAttribute] === this.value)].filter(Boolean)
|
||||
}
|
||||
},
|
||||
normalizedOptions () {
|
||||
return this.options.map(option => this.normalizeOption(option))
|
||||
},
|
||||
filteredNormalizedOptions () {
|
||||
let filteredNormalizedOptions = this.normalizedOptions
|
||||
|
||||
if (!this.q) {
|
||||
return filteredNormalizedOptions
|
||||
}
|
||||
|
||||
try {
|
||||
filteredNormalizedOptions = this.normalizedOptions.filter((option) => {
|
||||
return (this.searchAttributes?.length ? this.searchAttributes : [this.textAttribute]).some((searchAttribute) => {
|
||||
return option[searchAttribute] && option[searchAttribute].search(new RegExp(this.q, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
} catch (e) {}
|
||||
|
||||
return filteredNormalizedOptions
|
||||
}
|
||||
listContainerClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.container
|
||||
},
|
||||
watch: {
|
||||
disabled (value) {
|
||||
if (value && open) { this.close() }
|
||||
},
|
||||
open (value) {
|
||||
this.$emit('open', value)
|
||||
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.searchable) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.$refs.input.focus()
|
||||
this.$refs.search.$refs.input.select()
|
||||
})
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
if (this.value.length) {
|
||||
this.active = this.filteredNormalizedOptions.findIndex(option => this.value.includes(option[this.valueAttribute]))
|
||||
}
|
||||
} else if (this.value) {
|
||||
this.active = this.filteredNormalizedOptions.findIndex(option => option[this.valueAttribute] === this.value)
|
||||
}
|
||||
|
||||
if (this.instance) {
|
||||
this.instance.destroy()
|
||||
this.instance = null
|
||||
}
|
||||
|
||||
this.instance = createPopper(this.$refs.container, this.$refs.tooltip, {
|
||||
strategy: this.strategy,
|
||||
placement: this.placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false,
|
||||
adaptive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: 8
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoView()
|
||||
})
|
||||
},
|
||||
filteredNormalizedOptions () {
|
||||
this.updateActive()
|
||||
},
|
||||
q () {
|
||||
this.updateActive()
|
||||
}
|
||||
listWidthClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.width
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.instance) {
|
||||
this.instance.destroy()
|
||||
this.instance = null
|
||||
}
|
||||
listInputClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.input
|
||||
},
|
||||
methods: {
|
||||
toggle () {
|
||||
this.open = !this.open
|
||||
},
|
||||
close () {
|
||||
this.open = false
|
||||
},
|
||||
newOption () {
|
||||
this.$emit('new', this.q)
|
||||
},
|
||||
isOptionSelected (option) {
|
||||
if (this.multiple) {
|
||||
return this.value && this.value.find(it => it === option[this.valueAttribute])
|
||||
}
|
||||
|
||||
return this.value && this.value === option[this.valueAttribute]
|
||||
},
|
||||
selectOption (option) {
|
||||
if (this.multiple) {
|
||||
const value = [...this.value]
|
||||
const index = value.findIndex(it => it === option[this.valueAttribute])
|
||||
if (index > -1) {
|
||||
value.splice(index, 1)
|
||||
} else {
|
||||
value.push(option[this.valueAttribute])
|
||||
}
|
||||
this.$emit('input', value)
|
||||
} else {
|
||||
if (this.isOptionSelected(option)) {
|
||||
if (this.unselectable) {
|
||||
this.$emit('input', null)
|
||||
}
|
||||
} else {
|
||||
this.$emit('input', option[this.valueAttribute])
|
||||
}
|
||||
this.open = false
|
||||
}
|
||||
},
|
||||
guessOptionValue (option) {
|
||||
return get(option, this.valueAttribute, get(option, this.textAttribute))
|
||||
},
|
||||
guessOptionText (option) {
|
||||
return get(option, this.textAttribute, get(option, this.valueAttribute))
|
||||
},
|
||||
normalizeOption (option) {
|
||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||
return {
|
||||
[this.valueAttribute]: option,
|
||||
[this.textAttribute]: option
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...option,
|
||||
[this.valueAttribute]: this.guessOptionValue(option),
|
||||
[this.textAttribute]: this.guessOptionText(option)
|
||||
}
|
||||
},
|
||||
prev () {
|
||||
if (this.active - 1 >= (this.showNewOption ? -1 : 0)) {
|
||||
this.active--
|
||||
}
|
||||
|
||||
this.scrollIntoView()
|
||||
},
|
||||
next () {
|
||||
if (this.active + 1 <= (this.filteredNormalizedOptions.length - 1)) {
|
||||
this.active++
|
||||
}
|
||||
|
||||
this.scrollIntoView()
|
||||
},
|
||||
enter () {
|
||||
if (this.active === -1) {
|
||||
if (this.showNewOption) {
|
||||
this.newOption()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const option = this.filteredNormalizedOptions[this.active]
|
||||
if (!option) {
|
||||
return
|
||||
}
|
||||
|
||||
this.selectOption(option)
|
||||
},
|
||||
scrollIntoView () {
|
||||
let child
|
||||
if (this.active === -1) {
|
||||
child = this.$refs['option-new']
|
||||
} else {
|
||||
child = this.$refs[`option-${this.active}`][0]
|
||||
}
|
||||
|
||||
if (!child) {
|
||||
return
|
||||
}
|
||||
|
||||
child.scrollIntoView({ block: 'nearest' })
|
||||
},
|
||||
updateActive () {
|
||||
this.active = this.showNewOption && !this.filteredNormalizedOptions.length ? -1 : 0
|
||||
}
|
||||
listTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.selectCustom.list.transition
|
||||
},
|
||||
listOptionBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.base
|
||||
},
|
||||
listOptionContainerClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.container
|
||||
},
|
||||
listOptionActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.active
|
||||
},
|
||||
listOptionInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.inactive
|
||||
},
|
||||
listOptionSelectedClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.selected
|
||||
},
|
||||
listOptionUnselectedClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.unselected
|
||||
},
|
||||
listOptionDisabledClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.disabled
|
||||
},
|
||||
listOptionEmptyClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.empty
|
||||
},
|
||||
listOptionIcon: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.name
|
||||
},
|
||||
listOptionIconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.base
|
||||
},
|
||||
listOptionIconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.active
|
||||
},
|
||||
listOptionIconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.inactive
|
||||
},
|
||||
listOptionIconSizeClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.size
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'open', 'close'])
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.selectCustom.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectCustomClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.selectCustom.size[props.size],
|
||||
$ui.selectCustom.spacing[props.size],
|
||||
$ui.selectCustom.appearance[props.appearance],
|
||||
$ui.selectCustom.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.selectCustom.icon.size[props.size],
|
||||
'mr-2'
|
||||
)
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() =>
|
||||
query.value === ''
|
||||
? props.options
|
||||
: props.options.filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.textAttribute]).some((searchAttribute: any) => {
|
||||
return option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const queryOption = computed(() => {
|
||||
return query.value === '' ? null : { [props.textAttribute]: query.value }
|
||||
})
|
||||
|
||||
const iconWrapperClass = classNames(
|
||||
$ui.selectCustom.icon.trailing.wrapper
|
||||
)
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
function resolveOptionClass ({ active, selected, disabled }: { active: boolean, selected: boolean, disabled?: boolean }) {
|
||||
return classNames(
|
||||
props.listOptionBaseClass,
|
||||
active ? props.listOptionActiveClass : props.listOptionInactiveClass,
|
||||
selected ? props.listOptionSelectedClass : props.listOptionUnselectedClass,
|
||||
disabled && props.listOptionDisabledClass
|
||||
)
|
||||
}
|
||||
|
||||
function resolveOptionIconClass ({ active }: { active: boolean }) {
|
||||
return classNames(
|
||||
props.listOptionIconBaseClass,
|
||||
active ? props.listOptionIconActiveClass : props.listOptionIconInactiveClass
|
||||
)
|
||||
}
|
||||
|
||||
function onUpdate (event: any) {
|
||||
if (query.value && searchInput.value?.$el) {
|
||||
query.value = ''
|
||||
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USelectCustom' }
|
||||
</script>
|
||||
|
||||
@@ -11,140 +11,148 @@
|
||||
:placeholder="placeholder"
|
||||
:autocomplete="autocomplete"
|
||||
:class="textareaClass"
|
||||
@input="onInput($event.target.value)"
|
||||
@input="onInput(($event.target as any).value)"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
autoresize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value) {
|
||||
return Object.keys($ui.textarea.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value) {
|
||||
return Object.keys($ui.textarea.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.textarea.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.textarea.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
autoresize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.textarea.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const textarea = ref(null)
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.textarea.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.textarea.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.textarea.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value.focus()
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const autoResize = () => {
|
||||
if (props.autoresize) {
|
||||
if (!textarea.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const autoResize = () => {
|
||||
if (props.autoresize) {
|
||||
const styles = window.getComputedStyle(textarea.value)
|
||||
const paddingTop = parseInt(styles.paddingTop)
|
||||
const paddingBottom = parseInt(styles.paddingBottom)
|
||||
const padding = paddingTop + paddingBottom
|
||||
const initialHeight = (parseInt(styles.height) - padding) / textarea.value.rows
|
||||
const scrollHeight = textarea.value.scrollHeight - padding
|
||||
const newRows = Math.ceil(scrollHeight / initialHeight)
|
||||
textarea.value.rows = props.rows
|
||||
|
||||
textarea.value.rows = newRows
|
||||
}
|
||||
}
|
||||
const styles = window.getComputedStyle(textarea.value)
|
||||
const paddingTop = parseInt(styles.paddingTop)
|
||||
const paddingBottom = parseInt(styles.paddingBottom)
|
||||
const padding = paddingTop + paddingBottom
|
||||
const lineHeight = parseInt(styles.lineHeight)
|
||||
const { scrollHeight } = textarea.value
|
||||
const newRows = (scrollHeight - padding) / lineHeight
|
||||
|
||||
const onInput = (value) => {
|
||||
autoResize()
|
||||
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
autoResize()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const textareaClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.textarea.size[props.size],
|
||||
$ui.textarea.spacing[props.size],
|
||||
$ui.textarea.appearance[props.appearance],
|
||||
!props.resize && 'resize-none',
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
textarea,
|
||||
onInput,
|
||||
textareaClass
|
||||
if (newRows > props.rows) {
|
||||
textarea.value.rows = newRows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
autoResize()
|
||||
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
nextTick(autoResize)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
autoResize()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const textareaClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.textarea.size[props.size],
|
||||
$ui.textarea.spacing[props.size],
|
||||
$ui.textarea.appearance[props.appearance],
|
||||
!props.resize && 'resize-none',
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTextarea' }
|
||||
</script>
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
:class="[active ? activeClass : inactiveClass, baseClass]"
|
||||
>
|
||||
<span :class="[active ? containerActiveClass : containerInactiveClass, containerBaseClass]">
|
||||
<span :class="[active ? iconActiveClass : iconInactiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<span v-if="iconOn" :class="[active ? iconActiveClass : iconInactiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<Icon :name="iconOn" :class="iconOnClass" />
|
||||
</span>
|
||||
<span :class="[active ? iconInactiveClass : iconActiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<span v-if="iconOff" :class="[active ? iconInactiveClass : iconActiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<Icon :name="iconOff" :class="iconOffClass" />
|
||||
</span>
|
||||
</span>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Switch } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -27,11 +27,11 @@ const props = defineProps({
|
||||
},
|
||||
iconOn: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: null
|
||||
},
|
||||
iconOff: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: null
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
@@ -90,3 +90,7 @@ const active = computed({
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UToggle' }
|
||||
</script>
|
||||
|
||||
@@ -22,86 +22,93 @@
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils/'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.background
|
||||
},
|
||||
borderColorClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.border
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.ring
|
||||
},
|
||||
bodyClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.body
|
||||
},
|
||||
bodyBackgroundClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
headerClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.header
|
||||
},
|
||||
headerBackgroundClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
footerClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.footer
|
||||
},
|
||||
footerBackgroundClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
const props = defineProps({
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.background
|
||||
},
|
||||
borderColorClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.border
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.ring
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.rounded,
|
||||
validator (value: string) {
|
||||
return !value || ['sm', 'md', 'lg', 'xl', '2xl', '3xl'].map(size => `rounded-${size}`).includes(value)
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const cardClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.padded && props.rounded && 'rounded-md',
|
||||
!props.padded && props.rounded && 'sm:rounded-md',
|
||||
props.ringClass,
|
||||
props.shadowClass,
|
||||
props.backgroundClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
cardClass
|
||||
}
|
||||
bodyClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.body
|
||||
},
|
||||
bodyBackgroundClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
headerClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.header
|
||||
},
|
||||
headerBackgroundClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
footerClass: {
|
||||
type: String,
|
||||
default: () => $ui.card.footer
|
||||
},
|
||||
footerBackgroundClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const cardClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.padded && props.rounded && props.roundedClass,
|
||||
!props.padded && props.rounded && props.roundedClass && `sm:${props.roundedClass}`,
|
||||
props.ringClass,
|
||||
props.shadowClass,
|
||||
props.backgroundClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UCard',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,38 +4,35 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils/'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
constrained: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
constrainedClass: {
|
||||
type: String,
|
||||
default: () => $ui.container.constrained
|
||||
}
|
||||
const props = defineProps({
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
setup (props) {
|
||||
const containerClass = computed(() => {
|
||||
return classNames(
|
||||
'mx-auto sm:px-6 lg:px-8',
|
||||
props.padded && 'px-4',
|
||||
props.constrained && props.constrainedClass
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
containerClass
|
||||
}
|
||||
constrained: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
constrainedClass: {
|
||||
type: String,
|
||||
default: () => $ui.container.constrained
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const containerClass = computed(() => {
|
||||
return classNames(
|
||||
'mx-auto sm:px-6 lg:px-8',
|
||||
props.padded && 'px-4',
|
||||
props.constrained && props.constrainedClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UContainer' }
|
||||
</script>
|
||||
|
||||
225
src/runtime/components/navigation/CommandPalette.vue
Normal file
225
src/runtime/components/navigation/CommandPalette.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<Combobox
|
||||
ref="comboboxRef"
|
||||
:by="by"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:nullable="nullable"
|
||||
@update:model-value="onSelect"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<div v-show="searchable" class="relative flex items-center">
|
||||
<Icon :name="inputIcon" class="pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400" aria-hidden="true" />
|
||||
<ComboboxInput
|
||||
ref="comboboxInput"
|
||||
:value="query"
|
||||
class="w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm"
|
||||
:placeholder="inputPlaceholder"
|
||||
autocomplete="off"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
|
||||
<Button v-if="closeIcon" :icon="closeIcon" variant="transparent" class="absolute right-3" @click="onClear" />
|
||||
</div>
|
||||
|
||||
<ComboboxOptions v-if="groups.length" static hold class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2">
|
||||
<CommandPaletteGroup v-for="group of groups" :key="group.key" :group="group" :group-attribute="groupAttribute" :command-attribute="commandAttribute">
|
||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
</template>
|
||||
</CommandPaletteGroup>
|
||||
</ComboboxOptions>
|
||||
|
||||
<div v-else-if="placeholder" class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14">
|
||||
<Icon :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400" aria-hidden="true" />
|
||||
<p class="mt-4 text-sm text-center u-text-gray-900">
|
||||
{{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue'
|
||||
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
|
||||
import { useFuse } from '@vueuse/integrations/useFuse'
|
||||
import { groupBy, map } from 'lodash-es'
|
||||
import { defu } from 'defu'
|
||||
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||
import type { Group, Command } from '../../types/command-palette'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Button from '../elements/Button.vue'
|
||||
import CommandPaletteGroup from './CommandPaletteGroup.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: null
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
groups: {
|
||||
type: Array as PropType<Group[]>,
|
||||
default: () => []
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
inputIcon: {
|
||||
type: String,
|
||||
default: 'heroicons-outline:search'
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
},
|
||||
emptyIcon: {
|
||||
type: String,
|
||||
default: 'heroicons-outline:search'
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
commandAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<Partial<UseFuseOptions<Command>>>,
|
||||
default: () => ({})
|
||||
},
|
||||
autoselect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoclear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
placeholder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const query = ref('')
|
||||
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
|
||||
const comboboxApi = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoselect) {
|
||||
activateFirstOption()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const popoverProvides = comboboxInput.value?.$.provides
|
||||
if (!popoverProvides) {
|
||||
return
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
comboboxApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => defu({}, props.options, {
|
||||
fuseOptions: {
|
||||
keys: [props.commandAttribute]
|
||||
},
|
||||
resultLimit: 12,
|
||||
matchAllWhenSearchEmpty: true
|
||||
}))
|
||||
|
||||
const commands = computed(() => props.groups.reduce((acc, group) => {
|
||||
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
|
||||
}, [] as Command[]))
|
||||
|
||||
const { results } = useFuse(query, commands, options)
|
||||
|
||||
const groups = computed(() => map(groupBy(results.value, command => command.item.group), (results, key) => {
|
||||
const commands = results.map((result) => {
|
||||
const { item, ...data } = result
|
||||
|
||||
return {
|
||||
...item,
|
||||
...data
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...props.groups.find(group => group.key === key),
|
||||
commands: commands.slice(0, options.value.resultLimit)
|
||||
} as Group
|
||||
}))
|
||||
|
||||
watch(groups, () => {
|
||||
// Select first item on search changes
|
||||
setTimeout(() => {
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
|
||||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp' }))
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// Methods
|
||||
|
||||
function activateFirstOption () {
|
||||
// hack combobox by using keyboard event
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L769
|
||||
setTimeout(() => {
|
||||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function onSelect (option: Command | Command[]) {
|
||||
emit('update:modelValue', option, { query: query.value })
|
||||
|
||||
// Clear input after selection
|
||||
if (props.autoclear) {
|
||||
setTimeout(() => {
|
||||
query.value = ''
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function onClear () {
|
||||
if (query.value) {
|
||||
query.value = ''
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
updateQuery: (q: string) => {
|
||||
query.value = q
|
||||
},
|
||||
comboboxApi,
|
||||
results
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCommandPalette' }
|
||||
</script>
|
||||
87
src/runtime/components/navigation/CommandPaletteGroup.vue
Normal file
87
src/runtime/components/navigation/CommandPaletteGroup.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<li class="p-2">
|
||||
<h2 v-if="group[groupAttribute]" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
|
||||
{{ group[groupAttribute] }}
|
||||
</h2>
|
||||
|
||||
<ul class="text-sm u-text-gray-700">
|
||||
<ComboboxOption
|
||||
v-for="(command, index) of group.commands"
|
||||
:key="`${group.key}-${index}`"
|
||||
v-slot="{ active, selected }"
|
||||
:value="command"
|
||||
:disabled="command.disabled"
|
||||
as="template"
|
||||
>
|
||||
<li :class="['flex justify-between select-none items-center rounded-md px-3 py-2 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Icon v-if="command.icon" :name="command.icon" :class="['h-4 w-4 flex-shrink-0', active ? 'text-opacity-100 dark:text-opacity-100' : 'text-opacity-40 dark:text-opacity-40', command.iconClass || 'text-gray-900 dark:text-gray-50']" aria-hidden="true" />
|
||||
<Avatar
|
||||
v-else-if="command.avatar"
|
||||
v-bind="{ size: 'xxxs', ...command.avatar }"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
|
||||
|
||||
<div class="flex items-center gap-1.5 min-w-0" :class="{ 'opacity-50': command.disabled }">
|
||||
<slot :name="`${group.key}-command`" :group="group" :command="command">
|
||||
<span v-if="command.prefix" class="u-text-gray-400">{{ command.prefix }}</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-if="command.matches?.length" class="truncate" :class="{ 'flex-none': command.suffix }" v-html="highlight(command.matches[0])" />
|
||||
<span v-else class="truncate" :class="{ 'flex-none': command.suffix }">{{ command[commandAttribute] }}</span>
|
||||
<span v-if="command.suffix" class="u-text-gray-400 truncate">{{ command.suffix }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Icon v-if="selected" name="heroicons-outline:check" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
|
||||
<slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
|
||||
<span v-if="group.active" class="flex-shrink-0 u-text-gray-500">{{ group.active }}</span>
|
||||
</slot>
|
||||
<slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
|
||||
<span v-if="command.shortcuts?.length" class="flex-shrink-0 text-xs font-semibold u-text-gray-500">
|
||||
<kbd v-for="shortcut of command.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
||||
</span>
|
||||
<span v-else-if="!command.disabled && group.inactive" class="flex-shrink-0 u-text-gray-500">{{ group.inactive }}</span>
|
||||
</slot>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComboboxOption } from '@headlessui/vue'
|
||||
import type { PropType } from 'vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import type { Group } from '../../types/command-palette'
|
||||
|
||||
defineProps({
|
||||
group: {
|
||||
type: Object as PropType<Group>,
|
||||
required: true
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
commandAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
function highlight ({ indices, value }: { indices: number[][], value:string }, i = 1): string {
|
||||
const pair = indices[indices.length - i]
|
||||
if (!pair) {
|
||||
return value
|
||||
}
|
||||
|
||||
return `${highlight({ indices, value: value.substring(0, pair[0]) }, i + 1)}<mark>${value.substring(pair[0], pair[1] + 1)}</mark>${value.substring(pair[1] + 1)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCommandPaletteGroup' }
|
||||
</script>
|
||||
@@ -1,64 +1,49 @@
|
||||
<template>
|
||||
<nav class="flex items-center space-x-1.5">
|
||||
<div v-for="(link, index) of links" :key="index">
|
||||
<Button
|
||||
:size="size"
|
||||
:to="link.to"
|
||||
:label="link.label"
|
||||
:icon="link.icon"
|
||||
:variant="isActive(link) ? activeVariant : variant"
|
||||
:custom-class="isActive(link) ? activeClass : ''"
|
||||
@click="click(link)"
|
||||
/>
|
||||
</div>
|
||||
<nav :class="wrapperClass">
|
||||
<Link
|
||||
v-for="(link, index) of links"
|
||||
:key="index"
|
||||
:to="link.to"
|
||||
:exact="link.exact"
|
||||
:class="baseClass"
|
||||
:active-class="activeClass"
|
||||
:inactive-class="inactiveClass"
|
||||
>
|
||||
{{ link.label }}
|
||||
</Link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Button from '../elements/Button'
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Link from '../elements/Link.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Button
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array as PropType<{ to: RouteLocationNormalized, exact: boolean, label: string }[]>,
|
||||
required: true
|
||||
},
|
||||
props: {
|
||||
links: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md'
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'gray-hover'
|
||||
},
|
||||
activeVariant: {
|
||||
type: String,
|
||||
default: 'gray'
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'u-text-gray-700 hover:u-text-gray-700 focus:u-text-gray-700'
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.wrapper
|
||||
},
|
||||
computed: {
|
||||
options () {
|
||||
return this.links.map(link => ({ value: link.to, text: link.label }))
|
||||
}
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.base
|
||||
},
|
||||
methods: {
|
||||
click (link) {
|
||||
this.$emit('input', link)
|
||||
},
|
||||
isActive (link) {
|
||||
if (link.exact === false) {
|
||||
return !!this.$route.path.startsWith(link.to)
|
||||
} else {
|
||||
return this.$route.path === link.to
|
||||
}
|
||||
}
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.inactive
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UPills' }
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<nav class="flex items-center gap-6">
|
||||
<nav :class="wrapperClass">
|
||||
<Link
|
||||
v-for="(link, index) of links"
|
||||
:key="index"
|
||||
:to="link.to"
|
||||
:exact="link.exact"
|
||||
class="pt-2 pb-3 text-sm font-medium border-b-2 whitespace-nowrap"
|
||||
:class="baseClass"
|
||||
:active-class="activeClass"
|
||||
:inactive-class="inactiveClass"
|
||||
>
|
||||
@@ -14,26 +14,36 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Link from '../elements/Link'
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Link from '../elements/Link.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Link
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array as PropType<{ to: RouteLocationNormalized, exact: boolean, label: string }[]>,
|
||||
required: true
|
||||
},
|
||||
props: {
|
||||
links: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: 'u-border-black u-text-black hover:text-black dark:hover:text-white hover:border-black dark:hover:border-white'
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: 'border-transparent u-text-gray-500 hover:u-text-gray-700 hover:u-border-gray-300'
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.base
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.inactive
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTabs' }
|
||||
</script>
|
||||
|
||||
@@ -5,23 +5,30 @@
|
||||
v-slot="{ isActive }"
|
||||
:key="index"
|
||||
v-bind="link"
|
||||
:class="baseClass"
|
||||
:class="[baseClass, spacingClass].join(' ')"
|
||||
:active-class="activeClass"
|
||||
:inactive-class="inactiveClass"
|
||||
@click="link.click && link.click()"
|
||||
@keyup.enter="$event.target.blur()"
|
||||
>
|
||||
<slot name="icon" :link="link">
|
||||
<slot name="avatar" :link="link">
|
||||
<Avatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: 'xs', ...link.avatar }"
|
||||
:class="[avatarBaseClass, link.label && avatarSpacingClass]"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="icon" :link="link" :is-active="isActive">
|
||||
<Icon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
:class="[iconBaseClass, link.label && iconSpacingClass, isActive ? iconActiveClass : iconInactiveClass]"
|
||||
:class="[iconBaseClass, link.label && iconSpacingClass, isActive ? iconActiveClass : iconInactiveClass, link.iconClass]"
|
||||
/>
|
||||
</slot>
|
||||
<slot :link="link">
|
||||
<span v-if="link.label" class="truncate">{{ link.label }}</span>
|
||||
</slot>
|
||||
<slot name="badge" :link="link">
|
||||
<slot name="badge" :link="link" :is-active="isActive">
|
||||
<span v-if="link.badge" :class="[badgeBaseClass, isActive ? badgeActiveClass : badgeInactiveClass]">
|
||||
{{ link.badge }}
|
||||
</span>
|
||||
@@ -30,65 +37,88 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '../elements/Icon'
|
||||
import Link from '../elements/Link'
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Link from '../elements/Link.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
Link
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array as PropType<{
|
||||
to?: RouteLocationNormalized
|
||||
exact?: boolean
|
||||
label: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
avatar?: Partial<AvatarType>
|
||||
click?: Function
|
||||
badge?: string
|
||||
}[]>,
|
||||
required: true
|
||||
},
|
||||
props: {
|
||||
links: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.base
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.inactive
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.base
|
||||
},
|
||||
iconSpacingClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.spacing
|
||||
},
|
||||
iconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.active
|
||||
},
|
||||
iconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.inactive
|
||||
},
|
||||
badgeBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.base
|
||||
},
|
||||
badgeActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.active
|
||||
},
|
||||
badgeInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.inactive
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.base
|
||||
},
|
||||
spacingClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.spacing
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.inactive
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.base
|
||||
},
|
||||
iconSpacingClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.spacing
|
||||
},
|
||||
iconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.active
|
||||
},
|
||||
iconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.icon.inactive
|
||||
},
|
||||
avatarBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.avatar.base
|
||||
},
|
||||
avatarSpacingClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.avatar.spacing
|
||||
},
|
||||
badgeBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.base
|
||||
},
|
||||
badgeActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.active
|
||||
},
|
||||
badgeInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.inactive
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UVerticalNavigation' }
|
||||
</script>
|
||||
|
||||
75
src/runtime/components/overlays/ContextMenu.vue
Normal file
75
src/runtime/components/overlays/ContextMenu.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<div :class="baseClass">
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import type { VirtualElement } from '@popperjs/core'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
virtualElement: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.width
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.contextMenu.transition
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const virtualElement = toRef(props, 'virtualElement') as Ref<VirtualElement>
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.contextMenu.popperOptions))
|
||||
|
||||
const [, container] = usePopper(popperOptions.value, virtualElement)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UContextMenu' }
|
||||
</script>
|
||||
@@ -1,45 +1,40 @@
|
||||
<template>
|
||||
<TransitionRoot :show="isOpen" as="template">
|
||||
<Dialog @close="setIsOpen">
|
||||
<div class="fixed z-20 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<TransitionRoot :appear="appear" :show="isOpen" as="template">
|
||||
<Dialog :class="wrapperClass" @close="close">
|
||||
<TransitionChild
|
||||
v-if="overlay"
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="overlayTransition"
|
||||
>
|
||||
<div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
|
||||
</TransitionChild>
|
||||
|
||||
<div :class="innerClass" :style="innerStyle">
|
||||
<div :class="containerClass">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
:appear="appear"
|
||||
v-bind="modalTransition"
|
||||
>
|
||||
<DialogOverlay class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</TransitionChild>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Card
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
v-bind="$attrs"
|
||||
ring-class
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<slot />
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</Card>
|
||||
<DialogPanel :class="modalClass">
|
||||
<Card
|
||||
base-class=""
|
||||
background-class=""
|
||||
shadow-class=""
|
||||
ring-class=""
|
||||
rounded-class=""
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<slot />
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</Card>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,46 +42,131 @@
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Dialog, DialogOverlay, TransitionRoot, TransitionChild } from '@headlessui/vue'
|
||||
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
|
||||
import { classNames } from '../../utils'
|
||||
import Card from '../layout/Card.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
import Card from '../layout/Card'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
TransitionRoot,
|
||||
TransitionChild,
|
||||
Card
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
setIsOpen (value) {
|
||||
isOpen.value = value
|
||||
},
|
||||
toggleIsOpen () {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.wrapper
|
||||
},
|
||||
innerClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.inner
|
||||
},
|
||||
innerStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.container
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.background
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
overlayBackgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.overlay.background
|
||||
},
|
||||
overlayTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.modal.overlay.transition
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.ring
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.rounded
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.width
|
||||
},
|
||||
transition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.modal.transition
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const modalClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.widthClass,
|
||||
props.backgroundClass,
|
||||
props.shadowClass,
|
||||
props.ringClass,
|
||||
props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const overlayTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.overlayTransitionClass
|
||||
})
|
||||
|
||||
const modalTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.transitionClass
|
||||
})
|
||||
|
||||
function close (value: boolean) {
|
||||
isOpen.value = value
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UModal',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,42 +1,37 @@
|
||||
<template>
|
||||
<transition
|
||||
appear
|
||||
enter-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
|
||||
enter-active-class="transition duration-300 ease-out transform"
|
||||
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
|
||||
leave-class="opacity-100"
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<transition appear v-bind="transitionClass">
|
||||
<div
|
||||
class="z-50 w-full bg-white rounded-lg shadow-lg pointer-events-auto dark:bg-gray-800"
|
||||
:class="['z-50 w-full pointer-events-auto', backgroundClass, roundedClass, shadowClass]"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
<div class="relative overflow-hidden rounded-lg ring-1 u-ring-gray-200">
|
||||
<div :class="['relative overflow-hidden', roundedClass, ringClass]">
|
||||
<div class="p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<Icon :name="iconName" class="w-6 h-6" :class="iconClass" />
|
||||
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
|
||||
<div v-if="iconName" class="flex-shrink-0">
|
||||
<Icon :name="iconName" :class="iconClass" />
|
||||
</div>
|
||||
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p class="text-sm font-medium leading-5 u-text-gray-900">
|
||||
<div class="w-0 flex-1">
|
||||
<p class="text-sm font-medium u-text-gray-900">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p v-if="description" class="mt-1 text-sm leading-5 u-text-gray-500">
|
||||
{{ description }}
|
||||
</p>
|
||||
<Button
|
||||
v-if="undo"
|
||||
variant="white"
|
||||
size="xs"
|
||||
class="mt-2"
|
||||
@click.stop="onUndo"
|
||||
>
|
||||
Undo
|
||||
</Button>
|
||||
|
||||
<div v-if="description && actions.length" class="mt-3 flex items-center gap-6">
|
||||
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 dark:text-primary-400 hover:text-primary-400 dark:hover:text-primary-500" @click.stop="onAction(action)">
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ml-4">
|
||||
<div class="flex-shrink-0 flex items-center gap-3">
|
||||
<div v-if="!description && actions.length" class="flex items-center gap-2">
|
||||
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 dark:text-primary-400 hover:text-primary-400 dark:hover:text-primary-500" @click.stop="onAction(action)">
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="transition duration-150 ease-in-out u-text-gray-400 focus:outline-none hover:u-text-gray-500 focus:u-text-gray-500"
|
||||
@click.stop="onClose"
|
||||
@@ -55,148 +50,162 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
|
||||
|
||||
import Icon from '../elements/Icon'
|
||||
import Button from '../elements/Button'
|
||||
import type { PropType } from 'vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { useTimer } from '../../composables/useTimer'
|
||||
import { classNames } from '../../utils'
|
||||
import type { ToastNotificationAction } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
Button
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'info',
|
||||
validator (value) {
|
||||
return ['info', 'success', 'error', 'warning'].includes(value)
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
timeout: {
|
||||
type: Number,
|
||||
default: 5000
|
||||
},
|
||||
undo: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
callback: {
|
||||
type: Function,
|
||||
default: null
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.notification.type).includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup (props, { emit }) {
|
||||
let timer = null
|
||||
const remaining = ref(props.timeout)
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.ring
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.rounded
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.notification.transition
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.icon.base
|
||||
},
|
||||
timeout: {
|
||||
type: Number,
|
||||
default: 5000
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<{
|
||||
label: string,
|
||||
click: Function
|
||||
}[]>,
|
||||
default: () => []
|
||||
},
|
||||
callback: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
return props.icon || ({
|
||||
warning: 'heroicons-outline:exclamation-circle',
|
||||
info: 'heroicons-outline:information-circle',
|
||||
success: 'heroicons-outline:check-circle',
|
||||
error: 'heroicons-outline:x-circle'
|
||||
})[props.type]
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return ({
|
||||
warning: 'text-orange-400',
|
||||
info: 'text-blue-400',
|
||||
success: 'text-green-400',
|
||||
error: 'text-red-400'
|
||||
})[props.type] || 'u-text-gray-400'
|
||||
})
|
||||
let timer: any = null
|
||||
const remaining = ref(props.timeout)
|
||||
|
||||
const progressBarStyle = computed(() => {
|
||||
const remainingPercent = remaining.value / props.timeout * 100
|
||||
return { width: `${remainingPercent || 0}%` }
|
||||
})
|
||||
const iconName = computed(() => {
|
||||
return props.icon || $ui.notification.type[props.type]
|
||||
})
|
||||
|
||||
function onMouseover () {
|
||||
if (timer) {
|
||||
timer.pause()
|
||||
}
|
||||
}
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.notification.icon.color[props.type] || 'u-text-gray-400'
|
||||
)
|
||||
})
|
||||
|
||||
function onMouseleave () {
|
||||
if (timer) {
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
const progressBarStyle = computed(() => {
|
||||
const remainingPercent = remaining.value / props.timeout * 100
|
||||
return { width: `${remainingPercent || 0}%` }
|
||||
})
|
||||
|
||||
function onClose () {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
if (props.callback) {
|
||||
props.callback()
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function onUndo () {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
if (props.undo) {
|
||||
props.undo()
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.timeout) {
|
||||
return
|
||||
}
|
||||
|
||||
timer = useTimer(() => {
|
||||
onClose()
|
||||
}, props.timeout)
|
||||
|
||||
watchEffect(() => {
|
||||
remaining.value = timer.remaining.value
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
timer.stop()
|
||||
})
|
||||
|
||||
return {
|
||||
timer,
|
||||
iconName,
|
||||
iconClass,
|
||||
progressBarStyle,
|
||||
onMouseover,
|
||||
onMouseleave,
|
||||
onClose,
|
||||
onUndo
|
||||
}
|
||||
function onMouseover () {
|
||||
if (timer) {
|
||||
timer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseleave () {
|
||||
if (timer) {
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
|
||||
function onClose () {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
if (props.callback) {
|
||||
props.callback()
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function onAction (action: ToastNotificationAction) {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
if (action.click) {
|
||||
action.click()
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.timeout) {
|
||||
return
|
||||
}
|
||||
|
||||
timer = useTimer(() => {
|
||||
onClose()
|
||||
}, props.timeout)
|
||||
|
||||
watchEffect(() => {
|
||||
remaining.value = timer.remaining.value
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UNotification' }
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<template>
|
||||
<div class="fixed bottom-0 right-0 flex flex-col justify-end w-full z-55 sm:w-96">
|
||||
<div
|
||||
v-if="notifications.length"
|
||||
class="px-4 py-6 space-y-3 overflow-y-auto sm:px-6 lg:px-8"
|
||||
>
|
||||
<div class="fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96">
|
||||
<div v-if="notifications.length" class="px-4 py-6 space-y-3 overflow-y-auto sm:px-6">
|
||||
<div
|
||||
v-for="(notification, index) of notifications"
|
||||
v-show="index === notifications.length - 1"
|
||||
v-for="notification of notifications"
|
||||
:key="notification.id"
|
||||
>
|
||||
<Notification
|
||||
@@ -20,10 +16,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useNuxtApp, useState } from '#app'
|
||||
import Notification from './Notification'
|
||||
<script setup lang="ts">
|
||||
import type { ToastNotification } from '../../types'
|
||||
import Notification from './Notification.vue'
|
||||
import { useNuxtApp, useState } from '#imports'
|
||||
|
||||
const { $toast } = useNuxtApp()
|
||||
const notifications = useState('notifications')
|
||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UNotifications' }
|
||||
</script>
|
||||
|
||||
@@ -1,91 +1,135 @@
|
||||
<template>
|
||||
<Popover v-slot="{ open }" :class="wrapperClass">
|
||||
<PopoverButton ref="trigger" as="div">
|
||||
<slot :open="open">
|
||||
<Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave">
|
||||
<PopoverButton ref="trigger" as="div" class="inline-flex w-full" @mouseover="onMouseOver">
|
||||
<slot :open="open" :close="close">
|
||||
<button>Open</button>
|
||||
</slot>
|
||||
</PopoverButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="containerClass">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 translate-y-1"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel :class="panelClass" static>
|
||||
<slot name="panel" />
|
||||
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<PopoverPanel :class="baseClass" static>
|
||||
<slot name="panel" :open="open" :close="close" />
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
import { usePopper } from '../../utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel
|
||||
},
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
strategy: {
|
||||
type: String,
|
||||
default: 'fixed'
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: 'relative'
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: 'z-10'
|
||||
},
|
||||
panelClass: {
|
||||
type: String,
|
||||
default: 'transform'
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const [trigger, container] = usePopper({
|
||||
placement: props.placement,
|
||||
strategy: props.strategy,
|
||||
modifiers: [{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false,
|
||||
adaptive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: 8
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
return {
|
||||
trigger,
|
||||
container
|
||||
}
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.width
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.popover.transition
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.popover.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
|
||||
const popoverApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const popoverProvides = trigger.value?.$.provides
|
||||
if (!popoverProvides) {
|
||||
return
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
if (props.mode !== 'hover' || !popoverApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (popoverApi.value.popoverState === 0) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
popoverApi.value.togglePopover && popoverApi.value.togglePopover()
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
if (props.mode !== 'hover' || !popoverApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (popoverApi.value.popoverState === 1) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
popoverApi.value.closePopover && popoverApi.value.closePopover()
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UPopover' }
|
||||
</script>
|
||||
|
||||
136
src/runtime/components/overlays/Slideover.vue
Normal file
136
src/runtime/components/overlays/Slideover.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<TransitionRoot as="template" :appear="appear" :show="isOpen">
|
||||
<Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="isOpen = false">
|
||||
<TransitionChild
|
||||
v-if="overlay"
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="overlayTransition"
|
||||
>
|
||||
<div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
|
||||
</TransitionChild>
|
||||
|
||||
<TransitionChild
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="slideoverTransition"
|
||||
>
|
||||
<DialogPanel :class="slideoverClass">
|
||||
<div v-if="$slots.header" :class="headerClass">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<slot />
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { WritableComputedRef, PropType } from 'vue'
|
||||
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
validator: (value: string) => ['left', 'right'].includes(value)
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.background
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
overlayBackgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.overlay.background
|
||||
},
|
||||
overlayTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.slideover.overlay.transition
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.width
|
||||
},
|
||||
headerClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.header
|
||||
},
|
||||
transition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.slideover.transition
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const isOpen: WritableComputedRef<boolean> = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const slideoverClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.widthClass,
|
||||
props.backgroundClass
|
||||
)
|
||||
})
|
||||
|
||||
const overlayTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.overlayTransitionClass
|
||||
})
|
||||
|
||||
const slideoverTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
|
||||
enterTo: 'translate-x-0',
|
||||
leaveFrom: 'translate-x-0',
|
||||
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
|
||||
...props.transitionClass
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USlideover' }
|
||||
</script>
|
||||
@@ -1,97 +1,143 @@
|
||||
<template>
|
||||
<div ref="trigger" :class="wrapperClass" @mouseover="open = true" @mouseleave="open = false">
|
||||
<div ref="trigger" :class="wrapperClass" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
|
||||
<slot :open="open">
|
||||
Hover me
|
||||
</slot>
|
||||
|
||||
<div v-if="open" ref="container" :class="containerClass">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 translate-y-1"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 translate-y-1"
|
||||
>
|
||||
<div :class="tooltipClass">
|
||||
<div v-if="open && !prevent" ref="container" :class="[containerClass, widthClass]">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
|
||||
<slot name="text">
|
||||
{{ text }}
|
||||
<span class="truncate">{{ text }}</span>
|
||||
</slot>
|
||||
|
||||
<span v-if="shortcuts?.length" class="inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1">
|
||||
<span class="mr-1 u-text-gray-700">·</span>
|
||||
<kbd v-for="shortcut of shortcuts" :key="shortcut" class="flex items-center justify-center font-sans px-1 h-4 min-w-[16px] text-[10px] u-bg-gray-100 rounded u-text-gray-900">
|
||||
{{ shortcut }}
|
||||
</kbd>
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { usePopper } from '../../utils'
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom',
|
||||
validator: (value) => {
|
||||
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
|
||||
}
|
||||
},
|
||||
strategy: {
|
||||
type: String,
|
||||
default: 'fixed',
|
||||
validator: (value) => {
|
||||
return ['absolute', 'fixed'].includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: 'relative inline-flex'
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: 'z-10'
|
||||
},
|
||||
tooltipClass: {
|
||||
type: String,
|
||||
default: 'flex items-center justify-center invisible w-auto h-6 max-w-xs px-2 space-x-1 truncate rounded shadow lg:visible u-bg-gray-800 truncate u-text-gray-50 text-xs'
|
||||
}
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
setup (props) {
|
||||
const open = ref(false)
|
||||
const [trigger, container] = usePopper({
|
||||
placement: props.placement,
|
||||
strategy: props.strategy,
|
||||
modifiers: [{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
gpuAcceleration: false,
|
||||
adaptive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: 8
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
return {
|
||||
open,
|
||||
trigger,
|
||||
container
|
||||
}
|
||||
prevent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shortcuts: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.width
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.ring
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.rounded
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.tooltip.transition
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.tooltip.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
// Methods
|
||||
|
||||
function onMouseOver () {
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (open.value) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
open.value = true
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (!open.value) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
open.value = false
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTooltip' }
|
||||
</script>
|
||||
|
||||
82
src/runtime/composables/usePopper.ts
Normal file
82
src/runtime/composables/usePopper.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ref, onMounted, watchEffect } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { popperGenerator, defaultModifiers, VirtualElement } from '@popperjs/core/lib/popper-lite'
|
||||
import type { Instance } from '@popperjs/core'
|
||||
import { omitBy, isUndefined } from 'lodash-es'
|
||||
import flip from '@popperjs/core/lib/modifiers/flip'
|
||||
import offset from '@popperjs/core/lib/modifiers/offset'
|
||||
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'
|
||||
import computeStyles from '@popperjs/core/lib/modifiers/computeStyles'
|
||||
import eventListeners from '@popperjs/core/lib/modifiers/eventListeners'
|
||||
import { MaybeElement, unrefElement } from '@vueuse/core'
|
||||
import type { PopperOptions } from '../types'
|
||||
|
||||
export const createPopper = popperGenerator({
|
||||
defaultModifiers: [...defaultModifiers, offset, flip, preventOverflow, computeStyles, eventListeners]
|
||||
})
|
||||
|
||||
export function usePopper ({
|
||||
locked = false,
|
||||
overflowPadding = 8,
|
||||
offsetDistance = 8,
|
||||
offsetSkid = 0,
|
||||
gpuAcceleration = true,
|
||||
adaptive = true,
|
||||
scroll = true,
|
||||
resize = true,
|
||||
placement,
|
||||
strategy
|
||||
}: PopperOptions, virtualReference?: Ref<Element | VirtualElement>) {
|
||||
const reference = ref<MaybeElement>(null)
|
||||
const popper = ref<MaybeElement>(null)
|
||||
const instance = ref<Instance | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect((onInvalidate) => {
|
||||
if (!popper.value) { return }
|
||||
if (!reference.value && !virtualReference?.value) { return }
|
||||
|
||||
const popperEl = unrefElement(popper)
|
||||
const referenceEl = virtualReference?.value || unrefElement(reference)
|
||||
|
||||
// if (!(referenceEl instanceof HTMLElement)) { return }
|
||||
if (!(popperEl instanceof HTMLElement)) { return }
|
||||
if (!referenceEl) { return }
|
||||
|
||||
instance.value = createPopper(referenceEl, popperEl, omitBy({
|
||||
placement,
|
||||
strategy,
|
||||
modifiers: [{
|
||||
name: 'flip',
|
||||
enabled: !locked
|
||||
}, {
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: overflowPadding
|
||||
}
|
||||
}, {
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [offsetSkid, offsetDistance]
|
||||
}
|
||||
}, {
|
||||
name: 'computeStyles',
|
||||
options: {
|
||||
adaptive,
|
||||
gpuAcceleration
|
||||
}
|
||||
}, {
|
||||
name: 'eventListeners',
|
||||
options: {
|
||||
scroll,
|
||||
resize
|
||||
}
|
||||
}]
|
||||
}, isUndefined))
|
||||
|
||||
onInvalidate(instance.value.destroy)
|
||||
})
|
||||
})
|
||||
|
||||
return [reference, popper, instance] as const
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Ref, ref, computed } from 'vue-demi'
|
||||
import { ref, computed } from 'vue-demi'
|
||||
import { useTimestamp } from '@vueuse/core'
|
||||
|
||||
export function useTimer (cb: (...args: unknown[]) => any, interval: number) {
|
||||
let timer: number | null = null
|
||||
const timestamp = useTimestamp({ controls: true })
|
||||
const startTime: Ref<number | null> = ref(null)
|
||||
const startTime = ref<number | null>(null)
|
||||
|
||||
const remaining = computed(() => {
|
||||
if (!startTime.value) {
|
||||
@@ -17,7 +17,7 @@ export function useTimer (cb: (...args: unknown[]) => any, interval: number) {
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
startTime.value = null
|
||||
// eslint-disable-next-line node/no-callback-literal
|
||||
// eslint-disable-next-line n/no-callback-literal
|
||||
cb(...args)
|
||||
}, remaining.value) as unknown as number
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function useTimer (cb: (...args: unknown[]) => any, interval: number) {
|
||||
}
|
||||
|
||||
function resume () {
|
||||
startTime.value += (Date.now() - timestamp.timestamp.value)
|
||||
startTime.value = (startTime.value || 0) + (Date.now() - timestamp.timestamp.value)
|
||||
timestamp.resume()
|
||||
set()
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
[multiple],
|
||||
[type=date],
|
||||
[type=datetime-local],
|
||||
[type=email],
|
||||
[type=month],
|
||||
[type=number],
|
||||
[type=password],
|
||||
[type=search],
|
||||
[type=tel],
|
||||
[type=text],
|
||||
[type=time],
|
||||
[type=url],
|
||||
[type=week],
|
||||
select,
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
input:-ms-input-placeholder,
|
||||
textarea:-ms-input-placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1.5em
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right .5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact
|
||||
}
|
||||
|
||||
[multiple] {
|
||||
background-image: initial;
|
||||
background-position: initial;
|
||||
background-repeat: unset;
|
||||
background-size: initial;
|
||||
padding-right: .75rem;
|
||||
-webkit-print-color-adjust: unset;
|
||||
color-adjust: unset
|
||||
}
|
||||
|
||||
[type=checkbox],
|
||||
[type=radio] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
[type=checkbox] {
|
||||
border-radius: 0
|
||||
}
|
||||
|
||||
[type=radio] {
|
||||
border-radius: 100%
|
||||
}
|
||||
|
||||
[type=checkbox]:checked,
|
||||
[type=radio]:checked {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat
|
||||
}
|
||||
|
||||
[type=checkbox]:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
[type=radio]:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
[type=checkbox]:checked:focus,
|
||||
[type=checkbox]:checked:hover,
|
||||
[type=radio]:checked:focus,
|
||||
[type=radio]:checked:hover {
|
||||
border-color: transparent;
|
||||
background-color: currentColor
|
||||
}
|
||||
|
||||
[type=checkbox]:indeterminate {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat
|
||||
}
|
||||
|
||||
[type=checkbox]:indeterminate:focus,
|
||||
[type=checkbox]:indeterminate:hover {
|
||||
border-color: transparent;
|
||||
background-color: currentColor
|
||||
}
|
||||
|
||||
[type=file] {
|
||||
background: unset;
|
||||
border-color: inherit;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: unset;
|
||||
line-height: inherit
|
||||
}
|
||||
|
||||
[type=file]:focus {
|
||||
outline: 1px auto -webkit-focus-ring-color
|
||||
}
|
||||
33
src/runtime/plugins/clipboard.client.ts
Normal file
33
src/runtime/plugins/clipboard.client.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const { copy: copyToClipboard, isSupported } = useClipboard()
|
||||
|
||||
function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
|
||||
if (!isSupported) {
|
||||
return
|
||||
}
|
||||
|
||||
copyToClipboard(text).then(() => {
|
||||
if (!success.title && !success.description) {
|
||||
return
|
||||
}
|
||||
|
||||
nuxtApp.$toast.success(success)
|
||||
}, function (e) {
|
||||
nuxtApp.$toast.error({
|
||||
...failure,
|
||||
description: failure.description || e.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
clipboard: {
|
||||
copy
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,14 +1,12 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
import { Ref } from 'vue'
|
||||
import { defineNuxtPlugin, useState } from '#app'
|
||||
import { ToastNotification, ToastPlugin } from '../types/toast'
|
||||
import type { ToastNotification } from '../types'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const notifications: Ref<ToastNotification[]> = useState('notifications', () => [])
|
||||
export default defineNuxtPlugin(() => {
|
||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||
|
||||
function addNotification (notification: Partial<ToastNotification>) {
|
||||
const body = {
|
||||
id: nanoid(),
|
||||
id: new Date().getTime().toString(),
|
||||
...notification
|
||||
}
|
||||
|
||||
@@ -24,30 +22,24 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
|
||||
}
|
||||
|
||||
nuxtApp.provide('toast', {
|
||||
addNotification,
|
||||
removeNotification,
|
||||
success ({ title, description }: { title?: string, description?: string } = {}) {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title,
|
||||
description,
|
||||
timeout: 4000
|
||||
})
|
||||
},
|
||||
error ({ title = 'An error occurred!', description }: { title?: string, description?: string } = {}) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title,
|
||||
description,
|
||||
timeout: 4000
|
||||
})
|
||||
return {
|
||||
provide: {
|
||||
toast: {
|
||||
addNotification,
|
||||
removeNotification,
|
||||
success (notification: Partial<ToastNotification> = {}) {
|
||||
return addNotification({ type: 'success', ...notification })
|
||||
},
|
||||
info (notification: Partial<ToastNotification> = {}) {
|
||||
return addNotification({ type: 'info', ...notification })
|
||||
},
|
||||
warning (notification: Partial<ToastNotification> = {}) {
|
||||
return addNotification({ type: 'warning', ...notification })
|
||||
},
|
||||
error (notification: Partial<ToastNotification>) {
|
||||
return addNotification({ type: 'error', title: 'An error occurred!', ...notification })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
declare module '#app' {
|
||||
interface NuxtApp {
|
||||
$toast: ToastPlugin
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
574
src/runtime/presets/default.ts
Normal file
574
src/runtime/presets/default.ts
Normal file
@@ -0,0 +1,574 @@
|
||||
export default function defaultPreset (variantColors: string[]) {
|
||||
const button = {
|
||||
base: 'font-medium focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 focus:ring-offset-white dark:focus:ring-offset-black',
|
||||
rounded: 'rounded-md',
|
||||
size: {
|
||||
xxs: 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm leading-4',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
spacing: {
|
||||
xxs: 'px-2 py-1',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-2',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-6 py-3'
|
||||
},
|
||||
square: {
|
||||
xxs: 'p-1',
|
||||
xs: 'p-1.5',
|
||||
sm: 'p-2',
|
||||
md: 'p-2',
|
||||
lg: 'p-2',
|
||||
xl: 'p-3'
|
||||
},
|
||||
variant: {
|
||||
...variantColors.reduce((acc: any, color: string) => {
|
||||
acc[color] = `shadow-sm border border-transparent text-white bg-${color}-600 hover:bg-${color}-700 disabled:bg-${color}-600 focus:ring-2 focus:ring-offset-2 focus:ring-${color}-500`
|
||||
return acc
|
||||
}, {}),
|
||||
secondary: 'border border-transparent text-primary-700 bg-primary-100 hover:bg-primary-200 disabled:bg-primary-100 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
white: 'shadow-sm border u-border-gray-300 u-text-gray-700 u-bg-white hover:u-bg-gray-50 disabled:u-bg-white focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
gray: 'shadow-sm border u-border-gray-300 u-text-gray-700 u-bg-gray-50 hover:u-bg-gray-100 disabled:u-bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
black: 'shadow-sm border border-transparent u-text-white u-bg-gray-800 hover:u-bg-gray-900 disabled:u-bg-gray-800 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
transparent: 'border border-transparent u-text-gray-500 hover:u-text-gray-700 focus:u-text-gray-700 disabled:hover:u-text-gray-500',
|
||||
link: 'border border-transparent text-primary-500 hover:text-primary-700 focus:text-primary-700'
|
||||
},
|
||||
icon: {
|
||||
base: 'flex-shrink-0',
|
||||
loading: 'heroicons-outline:refresh',
|
||||
size: {
|
||||
xxs: 'h-3.5 w-3.5',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-5 w-5'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
xxs: '-ml-0.5 mr-1',
|
||||
xs: '-ml-0.5 mr-1.5',
|
||||
sm: '-ml-0.5 mr-2',
|
||||
md: '-ml-1 mr-2',
|
||||
lg: '-ml-1 mr-3',
|
||||
xl: '-ml-1 mr-3'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
xxs: 'ml-1 -mr-0.5',
|
||||
xs: 'ml-1.5 -mr-0.5',
|
||||
sm: 'ml-2 -mr-0.5',
|
||||
md: 'ml-2 -mr-1',
|
||||
lg: 'ml-3 -mr-1',
|
||||
xl: 'ml-3 -mr-1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const badge = {
|
||||
base: 'inline-flex items-center font-medium',
|
||||
size: {
|
||||
sm: 'text-xs px-2 py-0.5',
|
||||
md: 'text-sm px-2.5 py-0.5',
|
||||
lg: 'text-sm px-3 py-0.5',
|
||||
xl: 'text-sm px-4 py-1'
|
||||
},
|
||||
variant: {
|
||||
...variantColors.reduce((acc: any, color: string) => {
|
||||
acc[color] = `bg-${color}-100 dark:bg-${color}-700 text-${color}-800 dark:text-${color}-100`
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
const formGroup = {
|
||||
wrapper: '',
|
||||
label: 'block text-sm font-medium u-text-gray-700',
|
||||
labelWrapper: 'flex content-center justify-between',
|
||||
container: 'mt-1 relative',
|
||||
required: 'text-red-400',
|
||||
description: 'text-sm leading-5 u-text-gray-500',
|
||||
hint: 'text-sm leading-5 u-text-gray-500',
|
||||
help: 'mt-2 text-sm u-text-gray-500'
|
||||
}
|
||||
|
||||
const input = {
|
||||
wrapper: 'relative',
|
||||
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
|
||||
size: {
|
||||
xxs: 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm leading-4',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
spacing: {
|
||||
xxs: 'px-1 py-0.5',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-2',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-6 py-3'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
xxs: 'pl-7',
|
||||
xs: 'pl-7',
|
||||
sm: 'pl-10',
|
||||
md: 'pl-10',
|
||||
lg: 'pl-10',
|
||||
xl: 'pl-10'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
xxs: 'pr-7',
|
||||
xs: 'pr-7',
|
||||
sm: 'pr-10',
|
||||
md: 'pr-10',
|
||||
lg: 'pr-10',
|
||||
xl: 'pr-10'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
default: 'u-bg-white u-text-gray-700 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 dark:focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm',
|
||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
||||
},
|
||||
icon: {
|
||||
base: 'u-text-gray-400',
|
||||
loading: 'heroicons-outline:refresh',
|
||||
size: {
|
||||
xxs: 'h-3 w-3',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-5 w-5',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-5 w-5'
|
||||
},
|
||||
leading: {
|
||||
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
xxs: 'ml-2',
|
||||
xs: 'ml-2',
|
||||
sm: 'ml-2',
|
||||
md: 'ml-3',
|
||||
lg: 'ml-3',
|
||||
xl: 'ml-3'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
xxs: 'mr-2',
|
||||
xs: 'mr-2',
|
||||
sm: 'mr-2',
|
||||
md: 'mr-3',
|
||||
lg: 'mr-3',
|
||||
xl: 'mr-3'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textarea = {
|
||||
...input
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input
|
||||
}
|
||||
|
||||
const selectCustom = {
|
||||
...select,
|
||||
wrapper: 'relative',
|
||||
base: `${select.base} text-left cursor-default`,
|
||||
icon: {
|
||||
name: 'heroicons-solid:selector',
|
||||
...select.icon
|
||||
},
|
||||
list: {
|
||||
container: 'z-20',
|
||||
width: 'w-full',
|
||||
base: 'u-bg-white shadow-lg rounded-md ring-1 u-ring-gray-200 focus:outline-none overflow-y-auto py-1 max-h-60',
|
||||
input: 'relative block w-full focus:ring-transparent text-sm px-4 py-2 u-text-gray-700 border-l-0 u-bg-white border-t-0 border-r-0 u-border-gray-200 focus:u-border-gray-200',
|
||||
option: {
|
||||
base: 'cursor-default select-none relative py-2 pl-4 pr-10 text-sm group',
|
||||
container: 'flex items-center gap-3',
|
||||
active: 'text-white bg-primary-600',
|
||||
inactive: 'u-text-gray-900',
|
||||
selected: 'font-semibold',
|
||||
unselected: 'font-normal',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
empty: 'text-sm u-text-gray-400 px-4 py-2',
|
||||
icon: {
|
||||
name: 'heroicons-solid:check',
|
||||
base: 'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active: 'text-white',
|
||||
inactive: 'text-primary-600',
|
||||
size: 'h-5 w-5'
|
||||
}
|
||||
},
|
||||
transition: {
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
}
|
||||
},
|
||||
popperOptions: {
|
||||
placement: 'bottom-end'
|
||||
}
|
||||
}
|
||||
|
||||
const radio = {
|
||||
wrapper: 'relative flex items-start',
|
||||
base: 'h-4 w-4 text-primary-600 focus:ring-2 focus:ring-offset-2 u-bg-white dark:checked:bg-current dark:checked:border-transparent focus:ring-primary-500 focus:ring-offset-white dark:focus:ring-offset-black u-border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
label: 'font-medium u-text-gray-700',
|
||||
required: 'text-red-400',
|
||||
help: 'u-text-gray-500'
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...radio,
|
||||
base: `${radio.base} rounded`
|
||||
}
|
||||
|
||||
const card = {
|
||||
base: 'overflow-hidden',
|
||||
background: 'u-bg-white',
|
||||
border: 'u-border-gray-200',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
rounded: 'rounded-md',
|
||||
shadow: 'shadow',
|
||||
body: 'px-4 py-5 sm:p-6',
|
||||
header: 'px-4 py-5 sm:px-6',
|
||||
footer: 'px-4 py-4 sm:px-6'
|
||||
}
|
||||
|
||||
const modal = {
|
||||
wrapper: 'relative z-50',
|
||||
inner: 'fixed inset-0 overflow-y-auto',
|
||||
container: 'flex min-h-full items-end sm:items-center justify-center p-4 sm:p-0 text-center',
|
||||
base: 'relative inline-block align-bottom text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle w-full',
|
||||
background: 'u-bg-white',
|
||||
overlay: {
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
border: '',
|
||||
ring: '',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow-xl',
|
||||
width: 'sm:max-w-lg',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95',
|
||||
enterTo: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leaveTo: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
}
|
||||
}
|
||||
|
||||
const container = {
|
||||
constrained: 'max-w-7xl'
|
||||
}
|
||||
|
||||
const toggle = {
|
||||
base: 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 focus:ring-offset-white dark:focus:ring-offset-black',
|
||||
active: 'bg-primary-600',
|
||||
inactive: 'u-bg-gray-200',
|
||||
container: {
|
||||
base: 'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||
active: 'translate-x-5',
|
||||
inactive: 'translate-x-0'
|
||||
},
|
||||
icon: {
|
||||
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
|
||||
active: 'opacity-100 ease-in duration-200',
|
||||
inactive: 'opacity-0 ease-out duration-100',
|
||||
on: 'h-3 w-3 text-primary-600',
|
||||
off: 'h-3 w-3 u-text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const verticalNavigation = {
|
||||
wrapper: 'space-y-1',
|
||||
base: 'group flex items-center text-sm font-medium rounded-md w-full relative',
|
||||
spacing: 'px-3 py-2',
|
||||
active: 'u-text-gray-900 u-bg-gray-100',
|
||||
inactive: 'u-text-gray-600 hover:u-text-gray-900 hover:u-bg-gray-50 focus:u-bg-gray-50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-6 w-6',
|
||||
spacing: '-ml-1 mr-3',
|
||||
active: 'u-text-gray-500',
|
||||
inactive: 'u-text-gray-400 group-hover:u-text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
spacing: '-ml-1 mr-3'
|
||||
},
|
||||
badge: {
|
||||
base: 'ml-auto inline-block py-0.5 px-3 text-xs rounded-full',
|
||||
active: 'u-bg-white',
|
||||
inactive: 'u-bg-gray-100 u-text-gray-600 group-hover:u-bg-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
const alertDialog = {
|
||||
icon: {
|
||||
wrapper: 'h-12 w-12 sm:h-10 sm:w-10 bg-primary-100',
|
||||
base: 'text-primary-600'
|
||||
},
|
||||
title: 'text-lg leading-6 font-medium text-gray-900',
|
||||
description: 'text-sm text-gray-500'
|
||||
}
|
||||
|
||||
const dropdown = {
|
||||
wrapper: 'relative inline-flex text-left',
|
||||
container: 'z-20',
|
||||
width: 'w-48',
|
||||
base: 'u-bg-white divide-y u-divide-gray-100 rounded-md ring-1 u-ring-gray-200 shadow-lg focus:outline-none',
|
||||
group: 'py-1',
|
||||
item: {
|
||||
base: 'group flex items-center gap-3 px-4 py-2 text-sm w-full',
|
||||
active: 'u-bg-gray-100 u-text-gray-900',
|
||||
inactive: 'u-text-gray-700',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
icon: 'h-5 w-5 u-text-gray-400 group-hover:u-text-gray-500 flex-shrink-0',
|
||||
avatar: '-m-0.5 group-hover:u-bg-gray-200 flex-shrink-0',
|
||||
shortcuts: 'flex-shrink-0 text-xs font-semibold u-text-gray-500 ml-auto'
|
||||
},
|
||||
transition: {
|
||||
enterActiveClass: 'transition duration-100 ease-out',
|
||||
enterFromClass: 'transform scale-95 opacity-0',
|
||||
enterToClass: 'transform scale-100 opacity-100',
|
||||
leaveActiveClass: 'transition duration-75 ease-out',
|
||||
leaveFromClass: 'transform scale-100 opacity-100',
|
||||
leaveToClass: 'transform scale-95 opacity-0'
|
||||
},
|
||||
popperOptions: {
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = {
|
||||
wrapper: 'flex items-center gap-8',
|
||||
base: 'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm',
|
||||
active: 'border-primary-500 text-primary-600',
|
||||
inactive: 'border-transparent u-text-gray-500 hover:u-text-gray-700 hover:u-border-gray-300'
|
||||
}
|
||||
|
||||
const pills = {
|
||||
wrapper: 'flex items-center gap-4',
|
||||
base: 'px-3 py-2 font-medium text-sm rounded-md',
|
||||
active: 'u-bg-gray-100 u-text-gray-700',
|
||||
inactive: 'u-text-gray-500 hover:u-text-gray-700'
|
||||
}
|
||||
|
||||
const avatar = {
|
||||
wrapper: 'relative inline-flex items-center justify-center',
|
||||
background: 'u-bg-gray-100',
|
||||
rounded: 'rounded-md',
|
||||
placeholder: 'text-xs font-medium leading-none u-text-black truncate',
|
||||
size: {
|
||||
xxxs: 'h-4 w-4 text-xs',
|
||||
xxs: 'h-5 w-5 text-xs',
|
||||
xs: 'h-6 w-6 text-xs',
|
||||
sm: 'h-8 w-8 text-sm',
|
||||
md: 'h-10 w-10 text-md',
|
||||
lg: 'h-12 w-12 text-lg',
|
||||
xl: 'h-14 w-14 text-xl',
|
||||
'2xl': 'h-16 w-16 text-2xl',
|
||||
'3xl': 'h-20 w-20 text-3xl'
|
||||
},
|
||||
chip: {
|
||||
base: 'absolute block rounded-full ring-2 u-ring-white',
|
||||
position: {
|
||||
'top-right': 'top-0 right-0',
|
||||
'bottom-right': 'bottom-0 right-0',
|
||||
'top-left': 'top-0 left-0',
|
||||
'bottom-left': 'bottom-0 left-0'
|
||||
},
|
||||
variant: {
|
||||
...variantColors.reduce((acc: any, color: string) => {
|
||||
acc[color] = `bg-${color}-400`
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
size: {
|
||||
xxxs: 'h-1 w-1',
|
||||
xxs: 'h-1 w-1',
|
||||
xs: 'h-1.5 w-1.5',
|
||||
sm: 'h-2 w-2',
|
||||
md: 'h-2.5 w-2.5',
|
||||
lg: 'h-3 w-3',
|
||||
xl: 'h-3.5 w-3.5',
|
||||
'2xl': 'h-3.5 w-3.5',
|
||||
'3xl': 'h-4 w-4'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avatarGroup = {
|
||||
ring: 'ring-2 u-ring-white',
|
||||
margin: '-mr-1.5 first:mr-0'
|
||||
}
|
||||
|
||||
const slideover = {
|
||||
wrapper: 'fixed inset-0 flex z-40',
|
||||
overlay: {
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-in-out duration-500',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in-out duration-500',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
base: 'relative flex-1 flex flex-col w-full focus:outline-none',
|
||||
background: 'u-bg-white',
|
||||
width: 'max-w-md',
|
||||
header: 'flex items-center justify-between flex-shrink-0 px-4 sm:px-6 h-16 border-b u-border-gray-200',
|
||||
transition: {
|
||||
enter: 'transform transition ease-in-out duration-500 sm:duration-700',
|
||||
leave: 'transform transition ease-in-out duration-500 sm:duration-700'
|
||||
}
|
||||
}
|
||||
|
||||
const notification = {
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-lg',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
type: {
|
||||
info: 'heroicons-outline:information-circle',
|
||||
success: 'heroicons-outline:check-circle',
|
||||
warning: 'heroicons-outline:exclamation-circle',
|
||||
error: 'heroicons-outline:x-circle'
|
||||
},
|
||||
icon: {
|
||||
base: 'w-6 h-6',
|
||||
color: {
|
||||
warning: 'text-orange-400',
|
||||
info: 'text-blue-400',
|
||||
success: 'text-green-400',
|
||||
error: 'text-red-400'
|
||||
}
|
||||
},
|
||||
transition: {
|
||||
enterActiveClass: 'transform ease-out duration-300 transition',
|
||||
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
|
||||
enterToClass: 'translate-y-0 opacity-100 sm:translate-x-0',
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
}
|
||||
}
|
||||
|
||||
const tooltip = {
|
||||
wrapper: 'relative inline-flex',
|
||||
container: 'z-20',
|
||||
width: 'max-w-xs',
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow',
|
||||
rounded: 'rounded',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const popover = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
base: '',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const contextMenu = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
base: '',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popperOptions: {
|
||||
placement: 'bottom-start',
|
||||
scroll: false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
card,
|
||||
modal,
|
||||
button,
|
||||
badge,
|
||||
formGroup,
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
selectCustom,
|
||||
checkbox,
|
||||
radio,
|
||||
container,
|
||||
toggle,
|
||||
verticalNavigation,
|
||||
alertDialog,
|
||||
dropdown,
|
||||
tabs,
|
||||
pills,
|
||||
avatar,
|
||||
avatarGroup,
|
||||
slideover,
|
||||
notification,
|
||||
tooltip,
|
||||
popover,
|
||||
contextMenu
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPreset = ReturnType<typeof defaultPreset>
|
||||
@@ -1,294 +0,0 @@
|
||||
const colors = [
|
||||
'primary',
|
||||
'rose',
|
||||
'pink',
|
||||
'fuchsia',
|
||||
'purple',
|
||||
'violet',
|
||||
'indigo',
|
||||
'blue',
|
||||
'sky',
|
||||
'cyan',
|
||||
'teal',
|
||||
'emerald',
|
||||
'green',
|
||||
'lime',
|
||||
'yellow',
|
||||
'amber',
|
||||
'orange',
|
||||
'red'
|
||||
]
|
||||
|
||||
const button = {
|
||||
base: 'font-medium focus:outline-none disabled:cursor-not-allowed disabled:opacity-75',
|
||||
size: {
|
||||
xxs: 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm leading-4',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
spacing: {
|
||||
xxs: 'px-2 py-1',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-2',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-6 py-3'
|
||||
},
|
||||
square: {
|
||||
xxs: 'p-1',
|
||||
xs: 'p-1.5',
|
||||
sm: 'p-2',
|
||||
md: 'p-2',
|
||||
lg: 'p-2',
|
||||
xl: 'p-3'
|
||||
},
|
||||
variant: {
|
||||
...colors.reduce((acc: any, color) => {
|
||||
acc[color] = `shadow-sm border border-transparent text-white bg-${color}-600 hover:bg-${color}-700 disabled:bg-${color}-600 focus:ring-2 focus:ring-offset-2 focus:ring-${color}-500`
|
||||
return acc
|
||||
}, {}),
|
||||
primary: 'shadow-sm border border-transparent text-white bg-primary-600 hover:bg-primary-700 disabled:bg-primary-600 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
secondary: 'border border-transparent text-primary-700 bg-primary-100 hover:bg-primary-200 disabled:bg-primary-100 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
white: 'shadow-sm border u-border-gray-300 u-text-gray-700 u-bg-white hover:u-bg-gray-50 disabled:u-bg-white focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
transparent: 'border border-transparent u-text-gray-500 hover:u-text-gray-700 focus:u-text-gray-700 disabled:hover:u-text-gray-500',
|
||||
link: 'border border-transparent text-primary-500 hover:text-primary-700 focus:text-primary-700'
|
||||
},
|
||||
icon: {
|
||||
base: 'flex-shrink-0',
|
||||
loading: 'heroicons-outline:refresh',
|
||||
size: {
|
||||
xxs: 'h-3.5 w-3.5',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-5 w-5'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
xxs: '-ml-0.5 mr-1',
|
||||
xs: '-ml-0.5 mr-1.5',
|
||||
sm: '-ml-0.5 mr-2',
|
||||
md: '-ml-1 mr-2',
|
||||
lg: '-ml-1 mr-3',
|
||||
xl: '-ml-1 mr-3'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
xxs: 'ml-1 -mr-0.5',
|
||||
xs: 'ml-1.5 -mr-0.5',
|
||||
sm: 'ml-2 -mr-0.5',
|
||||
md: 'ml-2 -mr-1',
|
||||
lg: 'ml-3 -mr-1',
|
||||
xl: 'ml-3 -mr-1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const badge = {
|
||||
base: 'inline-flex items-center font-medium',
|
||||
size: {
|
||||
sm: 'text-xs px-2 py-0.5',
|
||||
md: 'text-sm px-2.5 py-0.5',
|
||||
lg: 'text-sm px-3 py-0.5',
|
||||
xl: 'text-sm px-4 py-1'
|
||||
},
|
||||
variant: {
|
||||
...colors.reduce((acc: any, color) => {
|
||||
acc[color] = `bg-${color}-100 dark:bg-${color}-700 text-${color}-800 dark:text-${color}-100`
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
const formGroup = {
|
||||
wrapper: '',
|
||||
label: 'block text-sm font-medium u-text-gray-700',
|
||||
labelWrapper: 'flex content-center justify-between',
|
||||
container: 'mt-1 relative',
|
||||
required: 'text-red-400',
|
||||
description: 'text-sm leading-5 u-text-gray-500',
|
||||
hint: 'text-sm leading-5 u-text-gray-500',
|
||||
help: 'mt-2 text-sm u-text-gray-500'
|
||||
}
|
||||
|
||||
const input = {
|
||||
wrapper: 'relative',
|
||||
base: 'block w-full u-bg-white u-text-gray-700 disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
|
||||
size: {
|
||||
xxs: 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm leading-4',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
spacing: {
|
||||
xxs: 'px-1 py-0.5',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-2',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-6 py-3'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
xxs: 'pl-7',
|
||||
xs: 'pl-7',
|
||||
sm: 'pl-10',
|
||||
md: 'pl-10',
|
||||
lg: 'pl-10',
|
||||
xl: 'pl-10'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
xxs: 'pr-7',
|
||||
xs: 'pr-7',
|
||||
sm: 'pr-10',
|
||||
md: 'pr-10',
|
||||
lg: 'pr-10',
|
||||
xl: 'pr-10'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
default: 'focus:ring-1 focus:ring-primary-500 focus:border-primary-500 dark:focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm',
|
||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
||||
},
|
||||
icon: {
|
||||
base: 'u-text-gray-400',
|
||||
loading: 'heroicons-outline:refresh',
|
||||
size: {
|
||||
xxs: 'h-3 w-3',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-5 w-5',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-5 w-5'
|
||||
},
|
||||
leading: {
|
||||
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
xxs: 'ml-2',
|
||||
xs: 'ml-2',
|
||||
sm: 'ml-3',
|
||||
md: 'ml-3',
|
||||
lg: 'ml-3',
|
||||
xl: 'ml-3'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
xxs: 'mr-2',
|
||||
xs: 'mr-2',
|
||||
sm: 'mr-3',
|
||||
md: 'mr-3',
|
||||
lg: 'mr-3',
|
||||
xl: 'mr-3'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textarea = {
|
||||
...input
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input
|
||||
}
|
||||
|
||||
const radio = {
|
||||
wrapper: 'relative flex items-start',
|
||||
base: 'h-4 w-4 text-primary-600 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 u-border-gray-300 dark:checked:border-primary-600 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
label: 'font-medium u-text-gray-700',
|
||||
required: 'text-red-400',
|
||||
help: 'u-text-gray-500'
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...radio,
|
||||
base: `${radio.base} rounded`
|
||||
}
|
||||
|
||||
const card = {
|
||||
base: 'overflow-hidden',
|
||||
background: 'u-bg-white',
|
||||
border: 'u-border-gray-200',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
shadow: '',
|
||||
body: 'px-4 py-5 sm:p-6',
|
||||
header: 'px-4 py-5 sm:px-6',
|
||||
footer: 'px-4 py-4 sm:px-6'
|
||||
}
|
||||
|
||||
const container = {
|
||||
constrained: 'max-w-7xl'
|
||||
}
|
||||
|
||||
const toggle = {
|
||||
base: 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
active: 'bg-primary-600',
|
||||
inactive: 'u-bg-gray-200',
|
||||
container: {
|
||||
base: 'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||
active: 'translate-x-5',
|
||||
inactive: 'translate-x-0'
|
||||
},
|
||||
icon: {
|
||||
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
|
||||
active: 'opacity-100 ease-in duration-200',
|
||||
inactive: 'opacity-0 ease-out duration-100',
|
||||
on: 'h-3 w-3 text-primary-600',
|
||||
off: 'h-3 w-3 u-text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const verticalNavigation = {
|
||||
wrapper: 'space-y-1',
|
||||
base: 'group flex items-center px-3 py-2 text-sm font-medium rounded-md w-full',
|
||||
active: 'u-text-gray-900 u-bg-gray-100',
|
||||
inactive: 'u-text-gray-600 hover:u-text-gray-900 hover:u-bg-gray-50 focus:u-bg-gray-50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-6 w-6',
|
||||
spacing: '-ml-1 mr-3',
|
||||
active: 'u-text-gray-500',
|
||||
inactive: 'u-text-gray-400 group-hover:u-text-gray-500'
|
||||
},
|
||||
badge: {
|
||||
base: 'ml-auto inline-block py-0.5 px-3 text-xs rounded-full',
|
||||
active: 'u-bg-white',
|
||||
inactive: 'u-bg-gray-100 u-text-gray-600 group-hover:u-bg-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
const alertDialog = {
|
||||
icon: {
|
||||
wrapper: 'h-12 w-12 sm:h-10 sm:w-10 bg-primary-100',
|
||||
base: 'text-primary-600'
|
||||
},
|
||||
title: 'text-lg leading-6 font-medium text-gray-900',
|
||||
description: 'text-sm text-gray-500'
|
||||
}
|
||||
|
||||
export default {
|
||||
card,
|
||||
button,
|
||||
badge,
|
||||
formGroup,
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
checkbox,
|
||||
radio,
|
||||
container,
|
||||
toggle,
|
||||
verticalNavigation,
|
||||
alertDialog
|
||||
}
|
||||
71
src/runtime/tailwind.css
Normal file
71
src/runtime/tailwind.css
Normal file
@@ -0,0 +1,71 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
@apply outline-primary-500;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.u-bg-white { @apply bg-white dark:bg-black; }
|
||||
.u-bg-gray-50 { @apply bg-gray-50 dark:bg-gray-900; }
|
||||
.u-bg-gray-100 { @apply bg-gray-100 dark:bg-gray-800; }
|
||||
.u-bg-gray-200 { @apply bg-gray-200 dark:bg-gray-700; }
|
||||
.u-bg-gray-300 { @apply bg-gray-300 dark:bg-gray-600; }
|
||||
.u-bg-gray-400 { @apply bg-gray-400 dark:bg-gray-500; }
|
||||
.u-bg-gray-500 { @apply bg-gray-500 dark:bg-gray-400; }
|
||||
.u-bg-gray-600 { @apply bg-gray-600 dark:bg-gray-300; }
|
||||
.u-bg-gray-700 { @apply bg-gray-700 dark:bg-gray-200; }
|
||||
.u-bg-gray-800 { @apply bg-gray-800 dark:bg-gray-100; }
|
||||
.u-bg-gray-900 { @apply bg-gray-900 dark:bg-gray-50; }
|
||||
.u-bg-black { @apply bg-black dark:bg-white; }
|
||||
.u-text-white { @apply text-white dark:text-black; }
|
||||
.u-text-gray-50 { @apply text-gray-50 dark:text-gray-900; }
|
||||
.u-text-gray-100 { @apply text-gray-100 dark:text-gray-800; }
|
||||
.u-text-gray-200 { @apply text-gray-200 dark:text-gray-700; }
|
||||
.u-text-gray-300 { @apply text-gray-300 dark:text-gray-600; }
|
||||
.u-text-gray-400 { @apply text-gray-400 dark:text-gray-500; }
|
||||
.u-text-gray-500 { @apply text-gray-500 dark:text-gray-400; }
|
||||
.u-text-gray-600 { @apply text-gray-600 dark:text-gray-300; }
|
||||
.u-text-gray-700 { @apply text-gray-700 dark:text-gray-200; }
|
||||
.u-text-gray-800 { @apply text-gray-800 dark:text-gray-100; }
|
||||
.u-text-gray-900 { @apply text-gray-900 dark:text-gray-50; }
|
||||
.u-text-black { @apply text-black dark:text-white; }
|
||||
.u-border-white { @apply border-white dark:border-black; }
|
||||
.u-border-gray-100 { @apply border-gray-100 dark:border-gray-900; }
|
||||
.u-border-gray-200 { @apply border-gray-200 dark:border-gray-800; }
|
||||
.u-border-gray-300 { @apply border-gray-300 dark:border-gray-700; }
|
||||
.u-border-gray-400 { @apply border-gray-400 dark:border-gray-600; }
|
||||
.u-border-gray-500 { @apply border-gray-500 dark:border-gray-500; }
|
||||
.u-border-gray-600 { @apply border-gray-600 dark:border-gray-400; }
|
||||
.u-border-gray-700 { @apply border-gray-700 dark:border-gray-300; }
|
||||
.u-border-gray-800 { @apply border-gray-800 dark:border-gray-200; }
|
||||
.u-border-gray-900 { @apply border-gray-900 dark:border-gray-100; }
|
||||
.u-border-black { @apply border-black dark:border-white; }
|
||||
.u-divide-white { @apply divide-white dark:divide-black; }
|
||||
.u-divide-gray-100 { @apply divide-gray-100 dark:divide-gray-900; }
|
||||
.u-divide-gray-200 { @apply divide-gray-200 dark:divide-gray-800; }
|
||||
.u-divide-gray-300 { @apply divide-gray-300 dark:divide-gray-700; }
|
||||
.u-divide-gray-400 { @apply divide-gray-400 dark:divide-gray-600; }
|
||||
.u-divide-gray-500 { @apply divide-gray-500 dark:divide-gray-500; }
|
||||
.u-divide-gray-600 { @apply divide-gray-600 dark:divide-gray-400; }
|
||||
.u-divide-gray-700 { @apply divide-gray-700 dark:divide-gray-300; }
|
||||
.u-divide-gray-800 { @apply divide-gray-800 dark:divide-gray-200; }
|
||||
.u-divide-gray-900 { @apply divide-gray-900 dark:divide-gray-100; }
|
||||
.u-divide-black { @apply divide-black dark:divide-white; }
|
||||
.u-ring-white { @apply ring-white dark:ring-black; }
|
||||
.u-ring-gray-100 { @apply ring-gray-100 dark:ring-gray-900; }
|
||||
.u-ring-gray-200 { @apply ring-gray-200 dark:ring-gray-800; }
|
||||
.u-ring-gray-300 { @apply ring-gray-300 dark:ring-gray-700; }
|
||||
.u-ring-gray-400 { @apply ring-gray-400 dark:ring-gray-600; }
|
||||
.u-ring-gray-500 { @apply ring-gray-500 dark:ring-gray-500; }
|
||||
.u-ring-gray-600 { @apply ring-gray-600 dark:ring-gray-400; }
|
||||
.u-ring-gray-700 { @apply ring-gray-700 dark:ring-gray-300; }
|
||||
.u-ring-gray-800 { @apply ring-gray-800 dark:ring-gray-200; }
|
||||
.u-ring-gray-900 { @apply ring-gray-900 dark:ring-gray-100; }
|
||||
.u-ring-black { @apply ring-black dark:ring-white; }
|
||||
}
|
||||
10
src/runtime/types/avatar.d.ts
vendored
Normal file
10
src/runtime/types/avatar.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Avatar {
|
||||
src: string
|
||||
alt: string
|
||||
text: string
|
||||
size: string
|
||||
rounded: boolean
|
||||
chip: boolean
|
||||
chipVariant: string
|
||||
chipPosition: string
|
||||
}
|
||||
3
src/runtime/types/clipboard.d.ts
vendored
Normal file
3
src/runtime/types/clipboard.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface ClipboardPlugin {
|
||||
copy: (text: string, success?: { title?: string, description?: string }, failure?: { title?: string, description?: string }) => void
|
||||
}
|
||||
27
src/runtime/types/command-palette.d.ts
vendored
Normal file
27
src/runtime/types/command-palette.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
|
||||
import type { Avatar } from './avatar'
|
||||
export interface Command {
|
||||
id: string | number
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
icon?: string
|
||||
iconClass?: string
|
||||
avatar?: Partial<Avatar>
|
||||
chip?: string
|
||||
disabled?: boolean
|
||||
shortcuts?: string[]
|
||||
group?: string
|
||||
score?: number
|
||||
matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
key: string
|
||||
active?: string
|
||||
inactive?: string
|
||||
commands: Command[]
|
||||
options?: Partial<UseFuseOptions<Command>>
|
||||
[key: string]: any
|
||||
}
|
||||
5
src/runtime/types/index.d.ts
vendored
Normal file
5
src/runtime/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './avatar'
|
||||
export * from './clipboard'
|
||||
export * from './command-palette'
|
||||
export * from './popper'
|
||||
export * from './toast'
|
||||
14
src/runtime/types/popper.d.ts
vendored
Normal file
14
src/runtime/types/popper.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Placement, PositioningStrategy } from '@popperjs/core'
|
||||
|
||||
export interface PopperOptions {
|
||||
locked?: boolean
|
||||
overflowPadding?: number
|
||||
offsetDistance?: number
|
||||
offsetSkid?: number
|
||||
placement?: Placement
|
||||
strategy?: PositioningStrategy
|
||||
gpuAcceleration?: boolean
|
||||
adaptive?: boolean
|
||||
scroll?: boolean
|
||||
resize?: boolean
|
||||
}
|
||||
8
src/runtime/types/toast.d.ts
vendored
8
src/runtime/types/toast.d.ts
vendored
@@ -1,3 +1,8 @@
|
||||
export interface ToastNotificationAction {
|
||||
label: string,
|
||||
click: Function
|
||||
}
|
||||
|
||||
export interface ToastNotification {
|
||||
id: string
|
||||
title: string
|
||||
@@ -5,7 +10,8 @@ export interface ToastNotification {
|
||||
type: string
|
||||
icon?: string
|
||||
timeout: number
|
||||
undo?: Function
|
||||
actions?: ToastNotificationAction[]
|
||||
click?: Function
|
||||
callback?: Function
|
||||
}
|
||||
|
||||
|
||||
18
src/runtime/utils/colors.ts
Normal file
18
src/runtime/utils/colors.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { omit, kebabCase } from './index'
|
||||
|
||||
export const colorsToExclude = [
|
||||
'inherit',
|
||||
'transparent',
|
||||
'current',
|
||||
'white',
|
||||
'black',
|
||||
'slate',
|
||||
'gray',
|
||||
'zinc',
|
||||
'neutral',
|
||||
'stone'
|
||||
]
|
||||
|
||||
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]
|
||||
|
||||
export const colorsAsRegex = (colors: string[]): string => colors.join('|')
|
||||
@@ -1,5 +1,16 @@
|
||||
export * from './popper'
|
||||
|
||||
export function classNames (...classes: any[string]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export const kebabCase = (str: string) => {
|
||||
return str
|
||||
?.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
||||
?.map(x => x.toLowerCase())
|
||||
?.join('-')
|
||||
}
|
||||
|
||||
export const omit = (obj: object, keys: string[]) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([key]) => !keys.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ref, onMounted, watchEffect } from 'vue'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
export function usePopper (options: object) {
|
||||
const reference = ref(null)
|
||||
const popper = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect((onInvalidate) => {
|
||||
if (!popper.value) { return }
|
||||
if (!reference.value) { return }
|
||||
|
||||
const popperEl = popper.value.el || popper.value
|
||||
const referenceEl = reference.value.el || reference.value
|
||||
|
||||
if (!(referenceEl instanceof HTMLElement)) { return }
|
||||
if (!(popperEl instanceof HTMLElement)) { return }
|
||||
|
||||
const { destroy } = createPopper(referenceEl, popperEl, options)
|
||||
|
||||
onInvalidate(destroy)
|
||||
})
|
||||
})
|
||||
|
||||
return [reference, popper]
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
{
|
||||
"extends": "./docs/.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": false,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"node",
|
||||
"@nuxt/kit"
|
||||
],
|
||||
"allowJs": true,
|
||||
"paths": {
|
||||
"#app": ["./node_modules/nuxt3/dist/app"]
|
||||
}
|
||||
}
|
||||
"module": "esnext"
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"./docs/.nuxt/nuxt.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user