Compare commits

..

473 Commits

Author SHA1 Message Date
Benjamin Canac
531a89cdb8 chore(release): 1.1.3 2023-03-02 11:02:04 +01:00
Benjamin Canac
6970c2d665 chore(CommandPalette): add slot for command icon 2023-02-28 16:49:50 +01:00
Benjamin Canac
9719ea3858 chore(release): 1.1.2 2023-02-28 11:58:28 +01:00
Benjamin Canac
a4af6b3805 chore(deps): bump 2023-02-28 10:50:50 +01:00
Benjamin Canac
3493c138d9 chore: split props for Dropdown, ContextMenu and Popover 2023-02-24 17:33:57 +01:00
Benjamin Canac
d08e64d53f fix(Tooltip): truncate 2023-02-22 15:59:02 +01:00
renovate[bot]
6aecb082d2 chore(deps): update devdependency vue-tsc to ^1.1.7 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 12:31:21 +01:00
Sylvain Marroufin
63e27f8b4b chore(docs): add ContextMenu component page (#128) 2023-02-22 12:31:08 +01:00
Benjamin Canac
7970aefcb0 fix(VerticalNavigation): links to type 2023-02-22 12:22:54 +01:00
renovate[bot]
a893d7fa2e chore(deps): update all non-major dependencies (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-21 19:26:03 +01:00
Benjamin Canac
8ace629ff8 chore(release): 1.1.1 2023-02-20 18:06:15 +01:00
Benjamin Canac
0d35b82ecb chore(CommandPalette): expose query 2023-02-20 18:05:52 +01:00
Benjamin Canac
5f37077835 types(CommandPalette): options no longer exists 2023-02-20 18:05:39 +01:00
Benjamin Canac
948f4b89b1 chore(release): 1.1.0 2023-02-17 19:08:09 +01:00
Benjamin Canac
e6d0dd5898 chore(CommandPalette): set debounce to 200 2023-02-17 19:07:38 +01:00
Benjamin Canac
4702a4f103 fix(CommandPalette): types 2023-02-17 18:14:11 +01:00
Benjamin Canac
efa9674815 feat(CommandPalette): handle async search for specific groups 2023-02-17 18:03:59 +01:00
Benjamin Canac
97d40395d3 chore(release): 1.0.0 2023-02-17 15:35:51 +01:00
Benjamin Canac
a2fb22d835 docs: ts-ignore module import 2023-02-17 14:53:35 +01:00
Benjamin Canac
d14a1a82c2 chore(deps): bump @egoist/tailwindcss-icons 2023-02-17 14:50:41 +01:00
Benjamin Canac
a566627a23 chore: bump deps 2023-02-17 14:26:20 +01:00
Benjamin Canac
ee3352278c feat: migrate to @egoist/tailwindcss-icons 2023-02-17 12:58:19 +01:00
Benjamin Canac
b1d9e01818 chore(release): 0.2.1 2023-02-16 17:02:23 +01:00
Benjamin Canac
ca171f3095 chore(ci): disable typecheck 2023-02-16 17:02:07 +01:00
Benjamin Canac
c0e493d96a chore(deps): bump nuxt-icon 2023-02-16 17:02:00 +01:00
Benjamin Canac
d0d3235860 chore(release): 0.2.0 2023-02-16 16:17:24 +01:00
Benjamin Canac
18915975be chore(release): 0.1.39 2023-02-16 16:16:57 +01:00
Benjamin Canac
f5d068be9d feat: use nuxt-icon 2023-02-16 16:16:37 +01:00
Benjamin Canac
6018f009a8 fix(SelectCustom): handle search on string arrays 2023-02-16 12:02:38 +01:00
Benjamin Canac
2b78b5d7dc chore(SelectCustom): improve options type 2023-02-16 11:53:19 +01:00
Benjamin Canac
87f3f0b4c0 chore(release): 0.1.38 2023-02-03 18:09:49 +01:00
Benjamin Canac
41bf56f2ae chore(tsconfig): remove override 2023-02-03 18:04:30 +01:00
Benjamin Canac
b7795f4ef6 chore(deps): add @types/node 2023-02-03 18:04:19 +01:00
Benjamin Canac
57f8145a8d chore(deps): bump 2023-02-03 17:51:06 +01:00
Benjamin Canac
70fbcb6b24 chore(release): 0.1.37 2023-02-03 16:02:21 +01:00
Kevin Marrec
bea47b5906 fix(CommandPalette): improve accessibility (#129) 2023-02-03 16:01:43 +01:00
Benjamin Canac
fc1b3b2f17 chore(release): 0.1.36 2023-02-02 15:10:25 +01:00
Benjamin Canac
5bf5a314c4 fix(CommandPalette): put back cursor on top only when query changes 2023-02-01 15:20:21 +01:00
Benjamin Canac
3558eb1a4f chore(release): 0.1.35 2023-02-01 14:38:15 +01:00
Benjamin Canac
1c4d46e056 fix(Dropdown): lint 2023-02-01 14:38:07 +01:00
Benjamin Canac
1b0ed9e732 docs: simplify popover panel template 2023-02-01 14:37:55 +01:00
Benjamin Canac
b72037a777 chore: handle disabled prop for Dropdown and Popover 2023-02-01 14:34:28 +01:00
Benjamin Canac
a7644860b8 fix(Dropdown): prevent panel display when no items 2023-01-31 12:33:27 +01:00
Benjamin Canac
c90cd9c4f3 fix(AvatarGroup): preset size prop 2023-01-28 01:49:24 +01:00
Benjamin Canac
7805168685 chore(release): 0.1.34 2023-01-27 14:10:46 +01:00
Benjamin Canac
27717a55b3 fix(CommandPalette): typecheck 2023-01-27 14:10:35 +01:00
Benjamin Canac
d651a22dce chore(release): 0.1.33 2023-01-27 13:02:18 +01:00
Benjamin Canac
c3ecbf4b20 chore(CommandPalette): start preset 2023-01-27 13:02:01 +01:00
Benjamin Canac
d8b10f3eef chore(release): 0.1.32 2023-01-23 14:24:36 +01:00
Benjamin Canac
1071b80b39 chore(deps): bump 2023-01-23 14:24:20 +01:00
Benjamin Canac
c5e9a1ef46 chore(release): 0.1.31 2023-01-17 15:35:38 +01:00
Benjamin Canac
afe69a570d chore(deps): bump 2023-01-17 15:35:16 +01:00
Benjamin Canac
e6ed834cea chore(release): 0.1.30 2023-01-17 15:00:54 +01:00
Benjamin Canac
30c5412a6b chore(CommandPalette): input close icon position 2023-01-17 15:00:21 +01:00
Benjamin Canac
01f56d9553 chore(Button): handle compact 2023-01-17 15:00:21 +01:00
Benjamin Canac
91f273c117 chore(release): 0.1.29 2023-01-17 15:00:19 +01:00
Benjamin Canac
cda8ce32a3 chore(release): 0.1.28 2023-01-13 18:33:18 +01:00
Benjamin Canac
2bc0eb05d1 chore(Slideover): emit event 2023-01-13 18:32:45 +01:00
Benjamin Canac
cfc4bdfbfe chore(release): 0.1.27 2023-01-12 15:33:35 +01:00
Sylvain Marroufin
370d05921d chore(lighthouse): improve CommandPalette 2023-01-12 14:28:29 +01:00
Sylvain Marroufin
b6455a151d chore(lighthouse): improve components accessibility (#127) 2023-01-12 12:32:42 +01:00
Benjamin Canac
8c0e0ec823 chore(release): 0.1.26 2023-01-09 18:43:45 +01:00
Sylvain Marroufin
4f56921096 fix(CommandPalette): select first item on search changes (#126) 2023-01-09 18:43:12 +01:00
Benjamin Canac
6a5ee32e05 chore(release): 0.1.25 2023-01-09 18:42:53 +01:00
Benjamin Canac
4ea07e1077 chore(release): 0.1.24 2023-01-04 12:17:13 +01:00
Benjamin Canac
5fd65d0917 chore(deps): bump 2023-01-04 11:39:24 +01:00
Benjamin Canac
2ec0cee1d9 chore(release): 0.1.23 2022-12-20 16:27:30 +01:00
Benjamin Canac
758e6f1400 Revert "chore: put back stop propagation on mode hover (#121)"
This reverts commit c015148f29.
2022-12-20 16:26:55 +01:00
Benjamin Canac
275fa1831d chore(release): 0.1.22 2022-12-19 18:27:36 +01:00
Benjamin Canac
8b5e08f6f2 chore(Tooltip): add prevent prop 2022-12-19 18:27:19 +01:00
Benjamin Canac
1635f57de6 chore(release): 0.1.21 2022-12-19 16:22:41 +01:00
Sylvain Marroufin
b3e0122001 chore(SelectCustom): add emit on open/close (#125) 2022-12-19 16:22:15 +01:00
Benjamin Canac
4f9d20e603 chore(release): 0.1.20 2022-12-19 11:37:46 +01:00
Benjamin Canac
f7add47cf2 chore(deps): pin @headlessui/vue version to 1.7.4 2022-12-19 11:37:28 +01:00
renovate[bot]
99c1c683eb Update all non-major dependencies (#122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-17 16:57:12 +01:00
Anthony Fu
ff9f6c251d fix: avoid referring to complex types in props (#123) 2022-12-17 16:56:49 +01:00
Benjamin Canac
e0c703ca6c chore(release): 0.1.19 2022-12-16 10:47:39 +01:00
Benjamin Canac
2210faa160 chore(deps): bump @headlessui/vue 2022-12-16 10:47:20 +01:00
Benjamin Canac
19589b5e05 chore(release): 0.1.18 2022-12-15 14:45:48 +01:00
Benjamin Canac
7051fa39a7 chore: add missing w-full on headlessui buttons 2022-12-15 14:45:14 +01:00
renovate[bot]
911278e95e chore(deps): update all non-major dependencies (#120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-13 13:55:28 +01:00
Sylvain Marroufin
c015148f29 chore: put back stop propagation on mode hover (#121) 2022-12-13 13:54:23 +01:00
Benjamin Canac
cb1fd55801 chore(release): 0.1.17 2022-12-06 16:56:31 +01:00
Benjamin Canac
16fd1c0ca3 fix: remove stop propagation on mode hover 2022-12-06 16:56:16 +01:00
Benjamin Canac
84ac92ed7a chore(release): 0.1.16 2022-12-06 16:25:01 +01:00
Benjamin Canac
0ade69de26 fix(Popover): preset from tooltip 2022-12-06 16:24:33 +01:00
Benjamin Canac
d9193abf55 chore(release): 0.1.15 2022-12-02 17:16:52 +01:00
Sylvain Marroufin
44a78d7f67 fix(Dropdown): better handle item click to preventDefault (#119) 2022-12-02 17:16:32 +01:00
Benjamin Canac
3ce600f89a chore(release): 0.1.14 2022-12-02 17:16:00 +01:00
Benjamin Canac
662dd9ee65 chore(release): 0.1.13 2022-12-01 14:08:26 +01:00
renovate[bot]
3be3751bfc chore(deps): update all non-major dependencies (#117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 13:02:31 +01:00
Sylvain Marroufin
5042b5806b chore(components): update props with optional fields (#118) 2022-12-01 13:01:45 +01:00
Benjamin Canac
2535301bcd chore(release): 0.1.12 2022-11-29 13:56:30 +01:00
Benjamin Canac
629bb72424 fix(Checkbox): types 2022-11-29 13:54:35 +01:00
Benjamin Canac
543b4e025f chore(release): 0.1.11 2022-11-29 12:56:04 +01:00
Benjamin Canac
7f18c6bdc7 fix(Checkbox): revert type fix as it breaks checkboxes 2022-11-29 12:55:44 +01:00
Benjamin Canac
0ea924ca0d chore(release): 0.1.10 2022-11-26 23:44:03 +01:00
renovate[bot]
22f3d42470 chore(deps): update dependency @nuxtjs/color-mode to ^3.2.0 (#63)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-26 23:43:10 +01:00
Benjamin Canac
1ad96065fd fix: default popper options 2022-11-26 23:42:36 +01:00
Anthony Fu
952786ed79 fix: default props for command palette (#116) 2022-11-26 23:41:25 +01:00
Benjamin Canac
9cf92e34ab chore(release): 0.1.9 2022-11-25 10:00:40 +01:00
Benjamin Canac
6321d3ed8d fix(Icon): couldn't load anymore 2022-11-25 10:00:30 +01:00
Benjamin Canac
bc0c168c0b fix(Icon): eslint ignore 2022-11-23 17:00:08 +01:00
Benjamin Canac
9975305f2a chore(deps): bump 2022-11-23 11:32:08 +01:00
Anthony Fu
edc1bd677b chore: improve types (#115)
* wip: improve types

* feat: improve types

* Apply suggestions from code review

Co-authored-by: Sylvain Marroufin <marroufin.sylvain@gmail.com>

* chore: update

* chore: enable ci typecheck

* chore: fix

Co-authored-by: Sylvain Marroufin <marroufin.sylvain@gmail.com>
2022-11-23 11:30:18 +01:00
Benjamin Canac
32d1f21299 chore(release): 0.1.8 2022-11-16 18:01:05 +01:00
Benjamin Canac
f901e417e1 chore(deps): bump nuxt to 3.0.0 2022-11-16 16:09:50 +01:00
Benjamin Canac
23921e1ae2 chore(release): 0.1.7 2022-11-16 10:57:06 +01:00
Benjamin Canac
8a5f52dbc8 chore(deps): bump 2022-11-16 10:56:50 +01:00
Benjamin Canac
04d5ce1c51 chore(release): 0.1.6 2022-11-15 12:08:10 +01:00
Benjamin Canac
3a300f72c1 fix(SelectCustom): add w-full on ComboboxButton 2022-11-10 14:37:37 +01:00
Benjamin Canac
927debfb6b chore(SelectCustom): add missing inline-flex on button 2022-11-10 13:54:43 +01:00
Benjamin Canac
44176ee897 chore(Notification): improve actions text color 2022-11-09 11:52:52 +01:00
Benjamin Canac
7f9d69183c chore(release): 0.1.5 2022-11-08 16:44:13 +01:00
Benjamin Canac
23e5f4c501 chore(CommandPalette): add autoclear prop 2022-11-08 16:43:18 +01:00
Benjamin Canac
24998e3ac5 chore(release): 0.1.4 2022-11-08 11:02:42 +01:00
Benjamin Canac
bebf18a89a chore(deps): bump 2022-11-08 11:02:22 +01:00
Anthony Fu
1b56b50d4d fix(button): support RouteLocationRaw type for to (#112) 2022-11-08 10:57:55 +01:00
Benjamin Canac
3337559b89 chore(release): 0.1.3 2022-11-04 12:21:08 +01:00
Benjamin Canac
d33a23ebc0 chore(deps): bump 2022-11-04 12:19:20 +01:00
Baptiste Leproux
28586c35a5 fix(types): add missing field in command palette type (#111) 2022-11-02 16:04:50 +01:00
Benjamin Canac
e521e1ac24 fix(docs): component input string field 2022-11-02 11:21:35 +01:00
Sylvain Marroufin
8ea3223071 chore(typescript): minor fixes (#110) 2022-11-02 10:50:40 +01:00
Benjamin Canac
4cbe983f61 chore(release): 0.1.2 2022-10-27 16:05:16 +02:00
Benjamin Canac
e42969f003 chore(Tooltip): improve design 2022-10-27 16:04:49 +02:00
Benjamin Canac
c93e37a0eb chore(Tooltip): handle shortcuts 2022-10-27 14:26:31 +02:00
Sylvain Marroufin
9e20f96b65 chore(Dropdown): don't show outline on keyboard navigation (#108) 2022-10-27 11:52:16 +02:00
Benjamin Canac
9f6f132a76 chore(release): 0.1.1 2022-10-26 13:13:13 +02:00
Sylvain Marroufin
5517cc2846 fix(Dropdown): close on click item with to (#103)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-10-26 13:05:41 +02:00
Benjamin Canac
8c24b5dc97 chore(module): add d.ts file 2022-10-26 13:04:36 +02:00
Benjamin Canac
ec8bd5cdc4 fix(SelectCustom): types and lint 2022-10-26 13:04:36 +02:00
Benjamin Canac
6fab68baa8 fix(CommandPalette): lint 2022-10-26 13:04:36 +02:00
Benjamin Canac
fc951e2980 chore(CommandPalette): improve types 2022-10-26 13:04:36 +02:00
Benjamin Canac
08ff6e6c09 docs: update 2022-10-26 13:04:36 +02:00
Sylvain Marroufin
9dcdaf474e chore(Dropdown|Popover|Tooltip): hover delay (#104) 2022-10-26 11:59:49 +02:00
Sylvain Marroufin
e9f0224b91 fix(Popover): avoid crash on mount if ref not loaded (#105) 2022-10-26 11:57:13 +02:00
Benjamin Canac
929192fd46 chore(deps): bump vueuse 2022-10-25 16:32:42 +02:00
Benjamin Canac
d9ff11d1a2 chore(release): 0.1.0 2022-10-25 14:52:45 +02:00
Benjamin Canac
80edb1bd4b chore(deps): bump 2022-10-25 14:11:27 +02:00
Sylvain Marroufin
fc658842bb chore: fix typescript errors (#102) 2022-10-25 14:09:54 +02:00
Benjamin Canac
32922def7d fix(CommandPalette): group items spacing 2022-10-24 16:46:19 +02:00
Benjamin Canac
a4bea1c508 chore(CommandPalette): reduce icon and avatar sizes 2022-10-24 16:19:27 +02:00
Benjamin Canac
f87252f05d fix(CommandPalette): prevent shortcuts to disappear on hover 2022-10-24 16:18:20 +02:00
Benjamin Canac
ec9f67094a fix(CommandPalette): reactivity issue + improve results
Resolves #95, resolves #96
2022-10-24 15:58:15 +02:00
Benjamin Canac
72dc0d0d0c fix(Modal): use object for innerStyle 2022-10-23 19:29:54 +02:00
Benjamin Canac
02b0a29d86 chore(Modal): add innerStyle prop 2022-10-23 19:26:30 +02:00
Benjamin Canac
cd2f1122be chore(Modal): add inner ref 2022-10-23 19:00:36 +02:00
Benjamin Canac
e3943123ce chore(deps): bump 2022-10-21 15:55:25 +02:00
Baptiste Leproux
d57647a77a fix(icon): hydratation warning when loading same icon twice (#99) 2022-10-20 11:34:58 +02:00
Benjamin Canac
aa97f26180 chore(deps): bump 2022-10-19 11:28:40 +02:00
Benjamin Canac
0b17fb02e4 chore(deps): bump 2022-10-18 11:24:11 +02:00
Baptiste Leproux
5039265097 chore: set popper options in preset (#97)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-10-18 11:13:28 +02:00
Benjamin Canac
c89c65bd44 chore(Dropdown): allow group preset change 2022-10-13 17:10:02 +02:00
Sylvain Marroufin
4515a1239b chore: expose all types in index.ts (#94) 2022-10-13 12:47:43 +02:00
Sylvain Marroufin
de9596bfc4 chore(Tooltip): update preset for truncate (#90) 2022-10-10 15:44:39 +02:00
Benjamin Canac
7da11ccec0 chore: improve Modal / Slideover preset 2022-10-09 21:35:53 +02:00
Benjamin Canac
d29377f614 chore(CommandPalette): support by prop and highlight 2022-10-09 13:31:11 +02:00
Benjamin Canac
f0e482cf01 chore(SelectCustom): support by prop 2022-10-09 13:30:24 +02:00
Benjamin Canac
056ab30474 fix(CommandPalette): prevent empty active slot 2022-10-09 12:54:45 +02:00
Benjamin Canac
95c14a4360 fix: default object options 2022-10-08 23:40:33 +02:00
Benjamin Canac
b25bb75783 chore(CommandPalette): show placeholder even when searchable 2022-10-08 23:31:35 +02:00
Benjamin Canac
98ae6c14aa docs: improve ContextMenu example 2022-10-08 23:22:23 +02:00
Benjamin Canac
8ee2a3cf5b chore(CommandPalette): expose to take control from textarea 2022-10-08 23:22:06 +02:00
Benjamin Canac
91f93c450e chore(ContextMenu): improve options 2022-10-08 22:24:57 +02:00
Benjamin Canac
237a673af3 chore(popper): add more options 2022-10-08 22:24:42 +02:00
Benjamin Canac
591e60dfc7 chore(deps): bump 2022-10-08 22:24:28 +02:00
Benjamin Canac
b9f0b3cb10 chore(ContextMenu): new component 2022-10-08 00:51:53 +02:00
Benjamin Canac
3ed64250cd chore(deps): bump 2022-10-08 00:37:26 +02:00
Benjamin Canac
f1b59fc59e chore: fix popper props merge 2022-10-07 13:40:23 +02:00
Benjamin Canac
005c18e4c0 chore: improve popper handling 2022-10-07 13:27:51 +02:00
Benjamin Canac
44b7199c4f chore(docs): improve 2022-10-07 13:27:42 +02:00
Sylvain Marroufin
4774adc0ae chore(SelectCustom): clear search on select (#88) 2022-10-06 15:56:20 +02:00
Sylvain Marroufin
03b0e07a53 chore(Tooltip): allow to disable popper flip (#87) 2022-10-06 15:54:28 +02:00
Sylvain Marroufin
2560088b2b chore(CommandPalette): allow to disable autoselect (#84) 2022-10-05 15:10:51 +02:00
Benjamin Canac
4ce991652b chore(Slideover): allow to disable transitions 2022-10-05 00:58:01 +02:00
Benjamin Canac
d96b4ea119 chore(preset): handle Modal / Slideover transitions 2022-10-05 00:24:44 +02:00
Benjamin Canac
b20c8c82ab chore(preset): handle transitions 2022-10-04 23:56:54 +02:00
Benjamin Canac
e5bf052c1f chore(deps): bump @nuxtjs/color-mode 2022-10-04 23:46:13 +02:00
Sylvain Marroufin
cf1b2cdd13 fix(SelectCustom): avoid submitting to form when closing (#83) 2022-10-04 16:48:31 +02:00
Benjamin Canac
8776929f63 chore(deps): bump 2022-10-03 17:50:27 +02:00
Daniel Roe
62361bfa8f fix: load icons on mount rather than within setup (#82) 2022-10-03 17:49:13 +02:00
Clément Ollivier
bf25ad15af chore: replace deprecated addAutoImportDir function (#81) 2022-10-03 10:26:12 +02:00
Benjamin Canac
03b18b1ff8 chore(deps): clean lock 2022-09-22 15:14:04 +02:00
Benjamin Canac
fdf149f0d0 chore(deps): bump 2022-09-22 15:11:09 +02:00
Benjamin Canac
e4c3a74e0e chore(Notification): add preset system 2022-09-20 11:47:52 +02:00
Benjamin Canac
38f248c24d chore(Modal): handle transition disable 2022-09-16 17:43:43 +02:00
Benjamin Canac
74de5b106b chore(Modal): improve preset 2022-09-16 16:57:44 +02:00
Benjamin Canac
70f8cd4889 chore(deps): remove nuxt-icon 2022-09-13 10:56:41 +02:00
Benjamin Canac
6a2159da94 Revert "chore: use nuxt-icon"
This reverts commit 70495e0a22.
2022-09-12 19:15:54 +02:00
Benjamin Canac
ae6b3c0ba6 chore(docs): set font-family like in tailwindui 2022-09-12 19:09:03 +02:00
Benjamin Canac
70495e0a22 chore: use nuxt-icon 2022-09-09 15:24:49 +02:00
Benjamin Canac
be99df3b30 chore(deps): bump 2022-09-09 15:24:49 +02:00
Sébastien Chopin
6870eff661 chore: update docs 2022-09-09 12:57:23 +02:00
Sébastien Chopin
062e468518 chore: remove not needed command 2022-09-09 12:19:16 +02:00
Benjamin Canac
3fc2a043fa chore(presets): handle radio and checkbox dark mode 2022-09-07 16:02:25 +02:00
Benjamin Canac
e99ec9b82b chore(VerticalNavigation): pass is-active to slots 2022-09-07 14:30:03 +02:00
Benjamin Canac
4556f24eb1 chore(deps): use nuxt rc.9 2022-09-05 11:03:48 +02:00
Benjamin Canac
f8fb66f177 chore(deps): upgrade @nuxt/kit 2022-09-05 10:58:35 +02:00
Benjamin Canac
5d760d9d8d chore(deps): bump 2022-09-05 10:54:09 +02:00
Benjamin Canac
cb5c9b3923 chore(package): try something for vercel deploy 2022-09-02 13:01:07 +02:00
Benjamin Canac
8e9418f94f chore(deps): bump @nuxt/kit 2022-09-02 13:00:56 +02:00
Benjamin Canac
4712726fbd docs: fix lint 2022-09-02 11:41:18 +02:00
Benjamin Canac
0cb38e2214 chore: prepare module for nuxt 3.0.0-rc9 (#77) 2022-09-02 11:37:59 +02:00
Sylvain Marroufin
3e9c502fbe chore(AvatarGroup): enhance avatars overlap (#75) 2022-08-29 18:57:11 +02:00
Benjamin Canac
541ed304a0 fix: error in Popover and Dropdown 2022-08-26 15:04:06 +02:00
Benjamin Canac
72919425b6 fix(Dropdown): increase timeout for hover mode 2022-08-17 14:14:13 +02:00
Benjamin Canac
8a66f5e9bf fix(Notification): prevent error without timeout 2022-08-17 14:10:34 +02:00
Benjamin Canac
a0d56d0f66 chore(Notifications): support actions instead of specific undo and dismiss 2022-08-17 13:53:53 +02:00
Benjamin Canac
c6706c76b2 chore(Dropdown): add item.iconClass 2022-08-12 17:19:52 +02:00
Benjamin Canac
1f7f28835d chore(Dropdown): pass event to item click 2022-08-12 17:16:00 +02:00
Benjamin Canac
593573a286 fix(Popover): inline-flex on trigger button 2022-08-12 10:27:13 +02:00
Benjamin Canac
3a6ecd6988 chore(SelectCustom): move icons in preset 2022-07-29 11:17:04 +02:00
Sylvain Marroufin
f9dbe28938 chore(typescript): fix typed imports (#72) 2022-07-25 18:45:37 +02:00
Benjamin Canac
2ae17ed919 chore(tailwind): override outline focus to primary color 2022-07-24 12:07:36 +02:00
Benjamin Canac
abb93b5fd3 fix(CommandPalette): command icons opacity in dark mode 2022-07-23 23:57:50 +02:00
Benjamin Canac
f75fc4f864 chore(Dropdown): support shortcuts 2022-07-23 23:53:13 +02:00
Benjamin Canac
5722a3ae62 fix(CommandPalette): icon inactive opacity on dark mode 2022-07-23 23:39:22 +02:00
Benjamin Canac
aa242aa87d fix(CommandPalette): truncate suffix 2022-07-22 18:06:11 +02:00
Benjamin Canac
945fec62c2 fix(Notification): improve placement with description 2022-07-22 17:07:37 +02:00
Sylvain Marroufin
9302b8d635 fix(CommandPalette): fix groups computed 2022-07-22 16:19:38 +02:00
Benjamin Canac
b43394ddc3 fix(CommandPalette): hack for reactivity 2022-07-22 15:47:32 +02:00
Benjamin Canac
be94fea84a fix: to prop type 2022-07-22 13:07:23 +02:00
Benjamin Canac
c08501db3f chore(VerticalNavigation): correct avatar size by default 2022-07-22 12:16:23 +02:00
Benjamin Canac
db2b73e741 chore(plugins): remove types imports 2022-07-21 23:37:05 +02:00
Benjamin Canac
6b813bd3b3 chore: improve types and imports 2022-07-21 23:33:06 +02:00
Benjamin Canac
7b3263a621 chore(deps): add vue-tsc 2022-07-21 23:32:46 +02:00
Benjamin Canac
002a8f6803 docs: refactor dropdown avatar 2022-07-21 23:32:33 +02:00
Benjamin Canac
5a596eb2b0 chore(CommandPalette): command avatar is now an object 2022-07-21 23:31:59 +02:00
Benjamin Canac
2f87cf86a1 chore(Dropdown): item avatar is now an object 2022-07-21 23:31:20 +02:00
Benjamin Canac
e4f148efa9 fix(CommandPalette): icon color on hover 2022-07-21 19:19:29 +02:00
Benjamin Canac
024a5914b9 chore(CommandPalette): align chip and avatar size with icon 2022-07-21 18:49:43 +02:00
Sylvain Marroufin
ce28b04187 chore(CommandPalette): improve customization options (#71)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-07-21 15:46:05 +02:00
Benjamin Canac
1ff9fd4f69 chore(toast): improve notifications 2022-07-21 00:39:06 +02:00
Benjamin Canac
487f253e14 chore(SelectCustom): handle disabled on button 2022-07-20 10:40:43 +02:00
Benjamin Canac
f54d4d788d chore(SelectCustom): add disabled prop 2022-07-20 10:15:54 +02:00
Benjamin Canac
c04885dd79 chore(release): 0.0.3 2022-07-18 19:49:04 +02:00
Benjamin Canac
8d70dbdcfe chore(deps): bump nuxt 2022-07-18 19:29:33 +02:00
Benjamin Canac
597439b538 chore(CommandPalette): put back nullable prop 2022-07-18 19:17:14 +02:00
Benjamin Canac
dc1050225b docs: update 2022-07-18 17:32:12 +02:00
Benjamin Canac
b44e3af422 chore(CommandPalette): improve component 2022-07-18 17:31:54 +02:00
Benjamin Canac
d9f08b99ca chore(CommandPalette): remove click and to handling 2022-07-18 16:04:47 +02:00
Benjamin Canac
af870f90ec chore(CommandPalette): remove timeout on query clear 2022-07-18 15:37:35 +02:00
Benjamin Canac
222bb987a4 chore(CommandPalette): improve component 2022-07-18 15:28:39 +02:00
Benjamin Canac
ea293bae0c fix(CommandPalette): add missing import 2022-07-18 15:12:02 +02:00
Benjamin Canac
8a57ab60c7 chore(CommandPalette): support selected with modelValue 2022-07-18 15:08:10 +02:00
Benjamin Canac
9e0edc27ab chore(CommandPalette): improve component 2022-07-18 14:31:23 +02:00
Benjamin Canac
76ffbf4cf3 fix(CommandPalette): options priority 2022-07-17 17:39:44 +02:00
Benjamin Canac
4eb4b65167 chore(CommandPalette): one fuse per group 2022-07-17 17:21:15 +02:00
Benjamin Canac
cf65b4ab54 fix(CommandPaletteGroup): invalid spacing when no icon 2022-07-17 14:47:24 +02:00
Benjamin Canac
c4e2197584 chore(deps): remove @types/tailwindcss 2022-07-17 14:45:35 +02:00
Benjamin Canac
1b5f7f5e31 chore(CommandPalette): use defu to merge options 2022-07-17 14:24:32 +02:00
Benjamin Canac
1495ff987d fix(CommandPaletteGroup): fail replace on items 2022-07-17 14:18:49 +02:00
Benjamin Canac
503b9a6b5c fix(CommandPalette): slice from computed options 2022-07-17 14:03:46 +02:00
Benjamin Canac
5e4c49ae3a chore(CommandPalette): improve options and always slice results 2022-07-17 14:00:42 +02:00
Benjamin Canac
c42dc9ecd2 chore(CommandPalette): set default threshold to undefined 2022-07-17 13:44:23 +02:00
Benjamin Canac
18dceb7445 feat(CommandPalette): implement component 2022-07-17 12:37:16 +02:00
Benjamin Canac
1a8ca6fb38 docs: fix input with objects and arrays 2022-07-17 12:36:59 +02:00
Benjamin Canac
2d7f096a55 chore(deps): bump 2022-07-17 12:36:36 +02:00
Benjamin Canac
da4e8d5c09 fix(SelectCustom): add missing bg in list input 2022-07-17 00:29:22 +02:00
Benjamin Canac
5fdf5eede0 chore(Slideover): add flex-shrink-0 to header 2022-07-16 23:07:49 +02:00
Benjamin Canac
d413cf74d6 fix(SelectCustom): improve creatable placeholder 2022-07-16 18:55:08 +02:00
Benjamin Canac
5906ee9c64 chore(SelectCustom): add preset for empty option 2022-07-16 18:48:51 +02:00
Benjamin Canac
0e0f3e39d3 fix(SelectCustom): remove unused import 2022-07-16 18:40:57 +02:00
Benjamin Canac
7907b1274d chore(SelectCustom): improve component 2022-07-16 18:38:56 +02:00
Benjamin Canac
46ea467098 fix(plugins): error in provides 2022-07-15 22:11:45 +02:00
Benjamin Canac
1d8958c2fc chore(deps): fix build 2022-07-13 23:51:04 +02:00
Benjamin Canac
9a52db84b7 chore(deps): bump 2022-07-13 23:26:33 +02:00
Benjamin Canac
78dad77a44 chore(package): prepare docs 2022-07-13 17:04:54 +02:00
Benjamin Canac
144b3382ad chore: fix lint 2022-07-13 16:58:12 +02:00
Benjamin Canac
b00a3aa7d6 chore(plugins): use return provide 2022-07-13 16:45:48 +02:00
Benjamin Canac
498be15a29 chore(Icon): improve component 2022-07-13 16:45:39 +02:00
Benjamin Canac
95c018f63f chore(Notifications): use #imports instead of #app 2022-07-13 16:45:01 +02:00
Benjamin Canac
efe9ab7140 chore(tsconfig): link to docs dir 2022-07-13 16:44:35 +02:00
Benjamin Canac
2dd9771d0d chore(deps): bump @tailwindcss/typography 2022-07-13 16:44:21 +02:00
Benjamin Canac
4de20563c3 chore(deps): bump 2022-07-12 15:25:14 +02:00
Benjamin Canac
40fef2bdf5 chore(Slideover): improve header 2022-07-11 22:32:11 +02:00
Sylvain Marroufin
00b9a0839b feat(AvatarGroup): preset support (#69) 2022-07-11 16:21:56 +02:00
Benjamin Canac
0e1b9087af chore(deps): bump 2022-07-11 16:12:41 +02:00
Sylvain Marroufin
5b4e4f8648 feat(Slideover): preset support (#68)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-07-11 16:00:45 +02:00
Benjamin Canac
6e616408ea chore(SelectCustom): add default gap on option container 2022-07-04 19:34:05 +02:00
Benjamin Canac
b1bbbbb7d0 chore(SelectCustom): improve option slot 2022-07-03 14:31:50 +02:00
Benjamin Canac
db39a9cdba fix(SelectCustom): move max-height on base 2022-07-02 19:55:23 +02:00
Benjamin Canac
524f8411c5 fix(SelectCustom): missing padding on list 2022-07-02 19:00:44 +02:00
Benjamin Canac
22ee717105 chore(SelectCustom): support popper and improve slots 2022-07-02 17:21:45 +02:00
Benjamin Canac
eb6fbd9c4a fix(SelectCustom): add missing text-sm class 2022-07-02 00:38:29 +02:00
Benjamin Canac
ae6e8eec03 fix(SelectCustom): add missing listContainerClass prop 2022-07-02 00:35:53 +02:00
Benjamin Canac
2e64343610 fix(SelectCustom): icon name in prop only for now 2022-07-02 00:33:05 +02:00
Benjamin Canac
6da0c28019 fix(SelectCustom): prop is icon instead of iconName 2022-07-02 00:28:36 +02:00
Benjamin Canac
8f9e0a4a50 chore(SelectCustom): allow override of icon 2022-07-02 00:26:25 +02:00
Benjamin Canac
06426ad1e5 chore(Button): support trailing and leading at once 2022-07-01 22:04:01 +02:00
Benjamin Canac
7db0ca50a0 fix(toast): id should be a string 2022-06-30 17:47:41 +02:00
Benjamin Canac
10d89d3cd1 fix(module): search in tailwind colors for primary and gray 2022-06-30 17:36:50 +02:00
Benjamin Canac
00e0ab39f8 fix(utils): types 2022-06-30 17:21:08 +02:00
Benjamin Canac
5a8b078d65 feat(module): handle variants with dynamic colors 2022-06-30 17:16:22 +02:00
Benjamin Canac
f98253c255 chore(Modal): increase z-index 2022-06-30 13:52:57 +02:00
Benjamin Canac
7f7b158b7e chore(tailwind): add dark color-scheme 2022-06-30 12:30:49 +02:00
Benjamin Canac
bbbe57dedb fix(module): register typography plugin 2022-06-30 11:50:20 +02:00
Benjamin Canac
a689542de5 chore(deps): add @tailwindcss/typography 2022-06-30 11:48:59 +02:00
Benjamin Canac
847788b2a0 chore(Avatar): allow override of rounded class 2022-06-29 00:12:51 +02:00
Benjamin Canac
d733e25bf0 fix(VerticalNavigation): link.avatar is now an object 2022-06-29 00:02:10 +02:00
Benjamin Canac
f0835cf979 fix(VerticalNavigation): remove avatarSize prop 2022-06-28 23:59:57 +02:00
Benjamin Canac
33bc561759 chore(VerticalNavigation): bind avatar props from link 2022-06-28 23:49:58 +02:00
Benjamin Canac
8039351bee chore(VerticalNavigation): handle avatar 2022-06-28 22:34:39 +02:00
Benjamin Canac
7979bc6586 chore(presets): update input icon spacing for sm 2022-06-28 15:28:46 +02:00
Sylvain Marroufin
2f90ce2319 feat(Slideover): add close button in header (#65) 2022-06-24 15:40:26 +02:00
Sylvain Marroufin
aecfef20e6 feat(Slideover): allow opening from the right side (#64) 2022-06-23 15:58:58 +02:00
Benjamin Canac
c3a200d450 chore(Modal): align modals at bottom on mobile 2022-06-23 14:16:24 +02:00
Benjamin Canac
dd6463710c fix(Card): prevent empty sm: class when rounded-class is null 2022-06-23 14:15:52 +02:00
Benjamin Canac
33b1176bdd fix(input): background should go into appearance 2022-06-21 15:54:32 +02:00
renovate[bot]
ce899b4e27 chore(deps): update all non-major dependencies (#60)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-06-20 15:42:50 +02:00
Benjamin Canac
536bfe4ff1 chore(SelectCustom): improve icon placement and list text size 2022-06-20 10:46:32 +02:00
Sylvain Marroufin
6585bfc24a fix(Avatar): truncate placeholder if too long (#61) 2022-06-07 16:49:13 +02:00
renovate[bot]
b05aa48caa chore(deps): update all non-major dependencies (#58)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-25 16:15:28 +02:00
Sylvain Marroufin
e419d68f64 fix(Select): normalizedValue handling Object modelValue (#59) 2022-05-24 10:59:27 +02:00
Benjamin Canac
06b07e292e fix(Card): prevent double class 2022-05-23 12:17:42 +02:00
Benjamin Canac
33a31205a1 chore(components): add names 2022-05-20 12:21:50 +02:00
Benjamin Canac
4f7e286790 chore(deps): bump 2022-05-20 12:18:47 +02:00
Sébastien Chopin
c1a7629e29 feat(Icon): support custom component and emoji
Co-Authored-By: Benjamin Canac <canacb1@gmail.com>
Co-Authored-By: Sylvain Marroufin <marroufin.sylvain@gmail.com>
2022-05-19 12:08:13 +02:00
Benjamin Canac
189c9f3eda chore(Select): add appearance prop 2022-05-17 18:02:16 +02:00
Benjamin Canac
358387e358 fix(module): remove safelist on max-w 2022-05-17 17:15:34 +02:00
Benjamin Canac
3c724e89d4 chore(Button): add rounded preset 2022-05-17 17:15:24 +02:00
Benjamin Canac
4851a4b905 Revert "chore(package): remove postbuild:docs"
This reverts commit 4f789ac54b.
2022-05-11 17:23:23 +02:00
Benjamin Canac
4f789ac54b chore(package): remove postbuild:docs 2022-05-11 14:29:20 +02:00
Benjamin Canac
72756689b9 chore(deps): bump @nuxtjs/color-mode 2022-05-10 10:57:19 +02:00
Sylvain Marroufin
d980176e03 fix(Modal): widthClass prop and default preset value (#56) 2022-05-09 17:59:04 +02:00
Sylvain Marroufin
23deef325a feat(toast): add aliases for info and warning notifications 2022-05-09 16:12:57 +02:00
renovate[bot]
4b4d16503f chore(deps): update all non-major dependencies (#54)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-09 16:12:13 +02:00
Benjamin Canac
0154fa6e8c chore(deps): move back to nuxt3 edge channel 2022-05-09 16:10:50 +02:00
Sylvain Marroufin
39bf242f78 chore: migrate components to typescript setup (#55)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-05-09 16:05:07 +02:00
Benjamin Canac
6c2f93f262 chore(Notifications): reduce padding on lg 2022-05-08 17:08:04 +02:00
Benjamin Canac
5031610d05 chore(VerticalNavigation): override link iconClass 2022-05-07 01:01:04 +02:00
Benjamin Canac
b9757384a5 chore(VerticalNavigation): add link class 2022-05-07 00:56:42 +02:00
Benjamin Canac
60c72a20d1 fix(Notifications): inexistant z-55 2022-05-06 18:41:48 +02:00
Benjamin Canac
879bf36698 chore(deps): bump @nuxtjs/tailwindcss 2022-05-05 11:15:31 +02:00
Benjamin Canac
f65f4aa19e Revert "fix(package): update postbuild:docs command"
This reverts commit 8045c7b47e.
2022-05-04 17:49:17 +02:00
Benjamin Canac
9651f4a216 Revert "chore(package): remove postbuild:docs command"
This reverts commit 051d089928.
2022-05-04 17:49:15 +02:00
Benjamin Canac
dd292afa5e chore(deps): bump 2022-05-04 17:47:30 +02:00
Benjamin Canac
538f707757 docs: fix ts error 2022-05-04 17:46:28 +02:00
Benjamin Canac
051d089928 chore(package): remove postbuild:docs command 2022-05-03 17:18:58 +02:00
Benjamin Canac
8045c7b47e fix(package): update postbuild:docs command 2022-05-03 17:16:25 +02:00
Benjamin Canac
0e18526a6f fix(useTimer): lint rule changed 2022-05-02 15:26:32 +02:00
Benjamin Canac
cc561f971b chore(deps): bump @nuxtjs/eslint-config-typescript 2022-05-02 15:20:46 +02:00
Benjamin Canac
c4a40b065c fix(Card): nullable validator on card roundedClass prop 2022-04-29 15:21:26 +02:00
Benjamin Canac
8222d05c15 fix(SelectCustom): move wrapper on top of Listbox 2022-04-28 17:56:46 +02:00
Benjamin Canac
7d1c38c820 chore(Slideover): remove lg:hidden 2022-04-28 15:06:07 +02:00
Benjamin Canac
dfe86f0baf fix(Modal): move classes to DialogPanel 2022-04-28 15:05:51 +02:00
Benjamin Canac
6532b4bfdd chore(Slideover): migrate by using DialogPanel 2022-04-28 14:36:45 +02:00
Benjamin Canac
936d6a5fee fix(SelectCustom): move wrapper under Listbox 2022-04-28 12:59:43 +02:00
Benjamin Canac
cde6b5037d chore(Modal): migrate by using DialogPanel 2022-04-28 12:44:33 +02:00
Benjamin Canac
5de5c07203 chore(SelectCustom): add multiple prop 2022-04-28 12:44:18 +02:00
Benjamin Canac
0f25820a49 docs: fix margin on example 2022-04-28 12:44:08 +02:00
renovate[bot]
f48dec0d60 chore(deps): update all non-major dependencies (#51)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-28 12:29:04 +02:00
Sylvain Marroufin
f0bfe20572 fix(textarea): autoresize reactivity (#52) 2022-04-28 11:39:03 +02:00
Sylvain Marroufin
1bda2ec01f chore(typescript): migrate components (#53) 2022-04-28 11:38:23 +02:00
Benjamin Canac
57354f64f9 chore(deps): migrate to nuxt 3.0.0-rc.1 2022-04-21 00:20:03 +02:00
Benjamin Canac
035919a545 fix(Link): add missing inactive-class on button and a 2022-04-20 18:31:59 +02:00
Benjamin Canac
18c194e839 fix(Tooltip): prevent close when hovering 2022-04-19 16:09:04 +02:00
Benjamin Canac
15b4f913ae chore(deps): bump 2022-04-19 16:08:53 +02:00
Benjamin Canac
723f075c56 fix(AvatarGroup): pass all avatar props 2022-04-15 11:59:40 +02:00
Benjamin Canac
39790058af chore(Popover): remove default panel-class prop 2022-04-12 16:01:59 +02:00
Benjamin Canac
2e099392f1 fix: Dropdown and Popover manual padding 2022-04-12 15:04:56 +02:00
Sylvain Marroufin
8bc4902078 fix: Hover mode on Dropdown & Popover (#48) 2022-04-12 11:44:35 +02:00
Benjamin Canac
656b6e1c59 fix(Popover): add missing onMounted import 2022-04-11 15:41:41 +02:00
Benjamin Canac
2eb2feab67 fix(Popover): add missing ref import 2022-04-11 15:39:24 +02:00
renovate[bot]
fc4de89dad chore(deps): update devdependency eslint to ^8.13.0 (#46)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-11 15:26:43 +02:00
Sylvain Marroufin
c431f8b4a1 feat(Popover): handle hovering mode (#47) 2022-04-11 15:26:33 +02:00
Benjamin Canac
289de07ba1 docs: add Slideover component 2022-04-08 10:56:16 +02:00
Benjamin Canac
368f93c294 chore(overlays): darken opacity overlay for modal and slideover 2022-04-08 10:56:16 +02:00
renovate[bot]
49afd2d404 chore(deps): update all non-major dependencies (#44)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-08 10:23:43 +02:00
Sylvain Marroufin
77149f0dd4 feat(Dropdown): add hover mode (#45) 2022-04-08 10:23:29 +02:00
Benjamin Canac
3ff8ae70b1 chore(Slideover): increase header padding on sm 2022-04-07 11:31:52 +02:00
Benjamin Canac
0d51747f4a chore(Popover): add close slot prop 2022-04-04 17:15:05 +02:00
renovate[bot]
5c45ac9882 chore(deps): update all non-major dependencies (#40)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-04 14:19:53 +02:00
Sylvain Marroufin
ba643d9faa fix(Textarea): autoresize (#43) 2022-04-04 14:19:23 +02:00
Benjamin Canac
571e907acd chore(Slideover): fix types 2022-03-31 10:56:36 +02:00
Benjamin Canac
065d3691ac chore(Slideover): move height to header child 2022-03-30 18:42:15 +02:00
Benjamin Canac
564ae86d63 chore(Slideover): improve header slot 2022-03-30 18:41:28 +02:00
Benjamin Canac
cf021a5888 fix(Slideover): remove useless padding 2022-03-30 18:29:44 +02:00
Benjamin Canac
7237dcb10f chore(Slideover): add component 2022-03-30 17:45:53 +02:00
Benjamin Canac
0ca40a57c9 chore(Dropdown): handle item hover
Co-Authored-By: Sébastien Chopin <seb@nuxtjs.com>
2022-03-26 00:45:16 +01:00
Benjamin Canac
3c7d986684 chore(deps): bump 2022-03-25 17:36:11 +01:00
Benjamin Canac
c59b45cf3c chore(VerticalNavigation): add relative class to base 2022-03-23 14:32:06 +01:00
renovate[bot]
685f738d5c chore(deps): update all non-major dependencies (#28)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-17 16:30:12 +01:00
Sylvain Marroufin
eb0043914b fix(Select): default value handling 2022-03-17 15:39:09 +01:00
Benjamin Canac
cc01af8e55 fix(Avatar): add missing watch import 2022-03-14 17:05:58 +01:00
Benjamin Canac
eb41b23432 fix(Avatar): missing ref import 2022-03-14 17:01:44 +01:00
Benjamin Canac
f38b920305 chore(Modal): add preset system 2022-03-14 16:55:19 +01:00
Benjamin Canac
dca0ff4a60 docs: improve slots component display 2022-03-14 16:55:19 +01:00
Sylvain Marroufin
fb3ff2e5fa fix(Avatar): url error handling (#39) 2022-03-14 16:51:16 +01:00
Benjamin Canac
9554e801c2 fix(popper): use $el after 1.5 upgrade
https://github.com/tailwindlabs/headlessui/discussions/1125#discussioncomment-2299441
2022-03-14 12:46:44 +01:00
Benjamin Canac
cdb4b1f233 chore(deps): bump 2022-03-14 12:46:15 +01:00
Benjamin Canac
3305f28c22 docs: remove useless example 2022-03-14 12:46:08 +01:00
Benjamin Canac
e8a1b46598 chore(AlertDialog): supper appear and close 2022-03-11 16:22:03 +01:00
Benjamin Canac
c71fc69b4b chore(Modal): add appear to transition child 2022-03-11 15:28:13 +01:00
Benjamin Canac
fc2d17cbc3 chore(Modal): add appear prop 2022-03-11 15:17:51 +01:00
Benjamin Canac
850c766fab fix(Modal): prevent attrs inherit 2022-03-10 16:33:40 +01:00
Benjamin Canac
da3ed26c2c fix(Avatar): prevent boolean src 2022-03-01 17:22:26 +01:00
Benjamin Canac
4d0709a25c docs: remove useless onSubmit 2022-03-01 17:03:16 +01:00
Benjamin Canac
7723704f79 fix(Select): disable placeholder 2022-03-01 17:01:48 +01:00
Benjamin Canac
77372423d8 fix(Link): use exact 2022-03-01 12:19:47 +01:00
Benjamin Canac
d4b65996ce fix(Link): send correct active 2022-03-01 12:15:23 +01:00
Benjamin Canac
55565d7079 chore(Avatar): use preset system 2022-02-25 17:57:55 +01:00
Benjamin Canac
858b7da3c0 chore(Avatar): add slot 2022-02-25 16:49:23 +01:00
Benjamin Canac
75440608d0 chore: remove unocss-include comments 2022-02-25 16:49:15 +01:00
Benjamin Canac
1e6ad72644 fix(SelectCustom): dark mode preset 2022-02-24 12:53:06 +01:00
Benjamin Canac
6cfdebb564 chore(module): transpile @iconify/vue 2022-02-24 12:52:51 +01:00
Benjamin Canac
7e85c07d72 chore(presets): remove nuxt 2022-02-23 16:25:26 +01:00
Benjamin Canac
a584d62119 chore(Link): remove inactiveClass when button or external 2022-02-23 16:21:12 +01:00
Benjamin Canac
ceedbe0bbd fix(Link): exact handling 2022-02-23 15:52:29 +01:00
Benjamin Canac
aef11562c9 fix(Link): handle isActive through vue-router 2022-02-23 15:49:07 +01:00
Benjamin Canac
09aed4b752 fix(SelectCustom): add tabindex -1 to hidden input 2022-02-23 12:47:44 +01:00
Benjamin Canac
0d98d44045 chore(Pills): refactor component 2022-02-23 12:38:58 +01:00
Sylvain Marroufin
c3facb1fef fix(Icon): missing imports 2022-02-22 12:00:21 +01:00
Sylvain Marroufin
78021d3850 fix(Icon): reload icon when prop name changes 2022-02-22 11:49:55 +01:00
Benjamin Canac
3d9ead3d8d chore(Tabs): use preset system 2022-02-22 11:42:06 +01:00
Benjamin Canac
5fb7f10283 fix(Dropdown): improve disabled state 2022-02-22 11:29:49 +01:00
Benjamin Canac
95b4b10c8e docs: fix dark mode blink 2022-02-22 11:22:15 +01:00
Benjamin Canac
b3e288aabc chore(deps): bump @nuxtjs/color-mode 2022-02-21 16:47:49 +01:00
Benjamin Canac
ba15aed671 docs: update 2022-02-18 16:35:47 +01:00
Benjamin Canac
7980e7f691 chore(deps): lock headlessui version to 1.4.3 for now 2022-02-18 16:35:41 +01:00
Baptiste Leproux
46ca8c5422 feat(button): Add black variant (#34)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-02-18 11:59:40 +01:00
Benjamin Canac
fc50834448 docs: update 2022-02-18 11:58:52 +01:00
Benjamin Canac
f1a830f7a6 chore(module): use @nuxtjs/color-mode 2022-02-18 11:58:38 +01:00
Benjamin Canac
3ab0698d7f fix(colors): move primary to safeColors 2022-02-18 11:31:43 +01:00
Benjamin Canac
6fc9466e8e docs: add github repo button 2022-02-18 11:31:30 +01:00
Benjamin Canac
6484575490 docs: update 2022-02-18 11:23:43 +01:00
Benjamin Canac
9406124ee1 chore(deps): bump 2022-02-18 11:23:43 +01:00
Sylvain Marroufin
4532e09ac0 feat(clipboard): replace navigator.clipboard with vueuse useClipboard (#33) 2022-02-17 17:48:13 +01:00
Benjamin Canac
e1d79d7fe7 fix(presets): add disabled bg color on nuxt buttons 2022-02-16 15:50:30 +01:00
Benjamin Canac
516e7faf8f fix(presets): dropdown avatar position 2022-02-16 12:30:42 +01:00
Benjamin Canac
c0661c3580 chore(Dropdown): support item avatar 2022-02-16 12:15:26 +01:00
Benjamin Canac
223a477a5e chore(SelectCustom): add slot on button 2022-02-15 12:54:22 +01:00
Benjamin Canac
8492e161db fix(SelectCustom): add default value to placeholder 2022-02-15 12:47:46 +01:00
Benjamin Canac
4e0d23ed34 fix(SelectCustom): handle placeholder when value is null 2022-02-15 12:45:37 +01:00
Benjamin Canac
13193969ee chore(presets): add shadow to card 2022-02-14 18:39:57 +01:00
Benjamin Canac
a01286f427 chore(deps): fix lock 2022-02-14 17:19:37 +01:00
Benjamin Canac
f67fdb7d2f fix(colors): hard-code colors as tailwindcss/colors is different 2022-02-14 16:59:55 +01:00
Benjamin Canac
541bcf6d76 chore(deps): bump and force tailwindcss version 2022-02-14 16:55:09 +01:00
Benjamin Canac
90ff1c0671 fix(Toggle): add v-if when icon props not defined 2022-02-14 16:54:57 +01:00
Benjamin Canac
4c891225b1 fix(module): use variants key for safelist 2022-02-14 16:34:33 +01:00
Benjamin Canac
c636ff24ba chore(module): improve safelist regex 2022-02-14 15:50:03 +01:00
Benjamin Canac
93c9fe1c74 fix(module): move colors utils to runtime dir 2022-02-14 15:26:34 +01:00
Benjamin Canac
4a648850f5 chore(module): safelist dynamic colors 2022-02-14 14:59:52 +01:00
Benjamin Canac
382e3b8e52 chore(Icon): improve component 2022-02-11 18:13:25 +01:00
Benjamin Canac
f513ea6ca8 fix(Icon): name can be an object 2022-02-11 17:59:23 +01:00
Benjamin Canac
b2705feedc fix(module): parse presets with mjs ext 2022-02-11 17:46:37 +01:00
Benjamin Canac
b8dd80d3d3 fix(module): presets content 2022-02-11 17:43:31 +01:00
Benjamin Canac
65aa1690ba fix(module): resolve runtime dir 2022-02-11 17:27:09 +01:00
Benjamin Canac
619f620b7e fix(SelectCustom): add missing required prop 2022-02-11 17:22:06 +01:00
Benjamin Canac
5b050ee52d chore(deps): move 2022-02-11 17:17:54 +01:00
Benjamin Canac
702abf7a9f feat: migrate to @nuxtjs/tailwindcss (#32) 2022-02-11 17:13:09 +01:00
Florent Delerue
1bec8d163c fix(Avatar): placeholder (#31) 2022-02-11 15:23:16 +01:00
Benjamin Canac
a9f1d937bc fix(presets): support dark ring-offset-color 2022-02-10 12:00:34 +01:00
Benjamin Canac
185273f3e9 fix(module): import colors from preset-mini 2022-02-10 12:00:23 +01:00
Benjamin Canac
757c38e75c chore(deps): bump 2022-02-10 12:00:08 +01:00
Sylvain Marroufin
6bd51975bf feat(toast): expose timeout to alias methods (#30)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2022-02-09 17:13:59 +01:00
Benjamin Canac
b7f6bf93d3 chore(presets): add card shadow 2022-02-09 17:11:48 +01:00
Benjamin Canac
b5edfaddd4 chore(presets): override container for nuxt preset 2022-02-08 18:41:30 +01:00
Benjamin Canac
45cf898ec3 fix(preset): replace avatar wrapper with inline-flex 2022-02-08 15:44:09 +01:00
Benjamin Canac
57d477f3a2 chore(Dropdown): add inline-flex on trigger 2022-02-07 14:48:39 +01:00
Benjamin Canac
d745d1aa9a chore(deps): bump 2022-02-07 14:48:27 +01:00
Benjamin Canac
5288dce744 docs: add favicon 2022-02-07 14:48:19 +01:00
Benjamin Canac
760da3d1a8 fix(Button): wrong config for icon size class 2022-02-04 17:02:34 +01:00
Sylvain Marroufin
832ffe4323 feat(plugins): clipboard (#29) 2022-02-02 17:14:41 +01:00
Benjamin Canac
ed499b3b21 fix(Avatar): remove gradient support 2022-02-02 14:32:57 +01:00
Benjamin Canac
5e44c54ae5 chore(module): exclude gradient-avatar from vite optimizeDeps 2022-02-02 12:16:44 +01:00
Benjamin Canac
d724f87654 chore(Dropdown): support preset 2022-02-02 12:15:01 +01:00
Benjamin Canac
af566ab1fa fix(Notifications): default value in useState 2022-02-02 12:06:27 +01:00
Benjamin Canac
85c8210ede fix(module): gradient-avatar include 2022-02-02 12:06:14 +01:00
Benjamin Canac
07df347fe3 chore(deps): bump 2022-02-02 11:56:40 +01:00
Benjamin Canac
e0342184fe fix(presets): defu merging 2022-02-01 14:52:37 +01:00
Benjamin Canac
e52d22de34 chore(VerticalNavigation): move spacing to specific prop 2022-02-01 14:28:56 +01:00
Benjamin Canac
a564f79b0c chore(module): add nuxt preset 2022-02-01 12:35:03 +01:00
66 changed files with 9598 additions and 6931 deletions

View File

@@ -41,6 +41,9 @@ jobs:
- name: Lint
run: yarn lint
- name: Typecheck
run: yarn typecheck
- name: Build
run: yarn build

View File

@@ -41,6 +41,9 @@ jobs:
- name: Lint
run: yarn lint
- name: Typecheck
run: yarn typecheck
- name: Build
run: yarn build

View File

@@ -2,6 +2,359 @@
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.
### [1.1.3](https://github.com/nuxtlabs/ui/compare/v1.1.2...v1.1.3) (2023-03-02)
### [1.1.2](https://github.com/nuxtlabs/ui/compare/v1.1.1...v1.1.2) (2023-02-28)
### Bug Fixes
* **Tooltip:** truncate ([d08e64d](https://github.com/nuxtlabs/ui/commit/d08e64d53fa439f34d51909bcb6812f1bcd95d83))
* **VerticalNavigation:** links `to` type ([7970aef](https://github.com/nuxtlabs/ui/commit/7970aefcb032ce01fcb11e9285fa61ce87f59519))
### [1.1.1](https://github.com/nuxtlabs/ui/compare/v1.1.0...v1.1.1) (2023-02-20)
## [1.1.0](https://github.com/nuxtlabs/ui/compare/v1.0.0...v1.1.0) (2023-02-17)
### Features
* **CommandPalette:** handle async search for specific groups ([efa9674](https://github.com/nuxtlabs/ui/commit/efa9674815ab4de756079690da0a381c3703d564))
### Bug Fixes
* **CommandPalette:** types ([4702a4f](https://github.com/nuxtlabs/ui/commit/4702a4f10379201c167cc52099519778756a5780))
## [1.0.0](https://github.com/nuxtlabs/ui/compare/v0.2.1...v1.0.0) (2023-02-17)
### Features
* migrate to `@egoist/tailwindcss-icons` ([ee33522](https://github.com/nuxtlabs/ui/commit/ee3352278cf03fdd12f2a4663b403052de3f089a))
### [0.2.1](https://github.com/nuxtlabs/ui/compare/v0.2.0...v0.2.1) (2023-02-16)
## [0.2.0](https://github.com/nuxtlabs/ui/compare/v0.1.39...v0.2.0) (2023-02-16)
### [0.1.39](https://github.com/nuxtlabs/ui/compare/v0.1.38...v0.1.39) (2023-02-16)
### Features
* use `nuxt-icon` ([f5d068b](https://github.com/nuxtlabs/ui/commit/f5d068be9d5778b3d4fcdc11d06d9d765e62075d))
### Bug Fixes
* **SelectCustom:** handle search on string arrays ([6018f00](https://github.com/nuxtlabs/ui/commit/6018f009a86cca196d15e4e72dd5eb41aaeb4bad))
### [0.1.38](https://github.com/nuxtlabs/ui/compare/v0.1.37...v0.1.38) (2023-02-03)
### [0.1.37](https://github.com/nuxtlabs/ui/compare/v0.1.36...v0.1.37) (2023-02-03)
### Bug Fixes
* **CommandPalette:** improve accessibility ([#129](https://github.com/nuxtlabs/ui/issues/129)) ([bea47b5](https://github.com/nuxtlabs/ui/commit/bea47b5906d1bc665717830d6dc2f3ff2a0374f3))
### [0.1.36](https://github.com/nuxtlabs/ui/compare/v0.1.35...v0.1.36) (2023-02-02)
### Bug Fixes
* **CommandPalette:** put back cursor on top only when query changes ([5bf5a31](https://github.com/nuxtlabs/ui/commit/5bf5a314c414b96c656190719bd56acca10676f5))
### [0.1.35](https://github.com/nuxtlabs/ui/compare/v0.1.34...v0.1.35) (2023-02-01)
### Bug Fixes
* **AvatarGroup:** preset size prop ([c90cd9c](https://github.com/nuxtlabs/ui/commit/c90cd9c4f37bc3ce5f6e13f3279dc2c574c76524))
* **Dropdown:** lint ([1c4d46e](https://github.com/nuxtlabs/ui/commit/1c4d46e056adf84d69462a12af8ac29f93cbf87a))
* **Dropdown:** prevent panel display when no items ([a764486](https://github.com/nuxtlabs/ui/commit/a7644860b8c22a0163e01ca2c0eab2c48b09745a))
### [0.1.34](https://github.com/nuxtlabs/ui/compare/v0.1.33...v0.1.34) (2023-01-27)
### Bug Fixes
* **CommandPalette:** typecheck ([27717a5](https://github.com/nuxtlabs/ui/commit/27717a55b3e5120f32fba2bcea30f5a91262f1c5))
### [0.1.33](https://github.com/nuxtlabs/ui/compare/v0.1.32...v0.1.33) (2023-01-27)
### [0.1.32](https://github.com/nuxtlabs/ui/compare/v0.1.31...v0.1.32) (2023-01-23)
### [0.1.31](https://github.com/nuxtlabs/ui/compare/v0.1.30...v0.1.31) (2023-01-17)
### [0.1.30](https://github.com/nuxtlabs/ui/compare/v0.1.29...v0.1.30) (2023-01-17)
### [0.1.29](https://github.com/nuxtlabs/ui/compare/v0.1.28...v0.1.29) (2023-01-17)
### [0.1.28](https://github.com/nuxtlabs/ui/compare/v0.1.27...v0.1.28) (2023-01-13)
### [0.1.27](https://github.com/nuxtlabs/ui/compare/v0.1.26...v0.1.27) (2023-01-12)
### [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)

View File

@@ -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
View 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'))
}
}
})

View File

@@ -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">
<ColorScheme placeholder="" tag="span">
<UButton variant="transparent" :icon="colorMode.value === 'dark' ? 'i-heroicons-moon' : 'i-heroicons-sun'" @click="toggleDark" />
</ColorScheme>
<UButton to="https://github.com/nuxtlabs/ui" target="_blank" variant="transparent" icon="i-mdi-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: '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: '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: 'ContextMenu', to: '/components/ContextMenu' }, { 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>

View File

@@ -1,44 +1,36 @@
/* @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'
app: {
head: {
htmlAttrs: {
lang: 'en'
}
}
},
buildModules: [
module
modules: [
// @ts-ignore
nuxtUI
],
components: {
global: true
},
ui: {
colors: {
primary: 'blue'
primary: 'blue',
gray: 'zinc'
},
preset: {
container: {
constrained: 'max-w-8xl'
}
},
unocss: {
icons: ['heroicons', 'mdi'],
tailwindcss: {
theme: {
fontFamily: {
sans: '"Inter var", sans-serif'
},
maxWidth: {
'8xl': '90rem'
extend: {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans]
}
}
}
}

View File

@@ -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 ? 'i-heroicons-clipboard-document-check' : 'i-heroicons-clipboard-document'"
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"
@@ -37,7 +56,6 @@
v-if="prop.type === 'Boolean'"
v-model="prop.value"
:name="prop.key"
:label="prop.key"
/>
<USelect
v-else-if="prop.values"
@@ -63,34 +81,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 +105,45 @@ 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 x = ref(0)
const y = ref(0)
const isContextMenuOpen = ref(false)
const virtualElement = ref({ getBoundingClientRect: () => ({}) })
onMounted(() => {
document.addEventListener('mousemove', ({ clientX, clientY }) => {
x.value = clientX
y.value = clientY
})
})
function openContextMenu () {
const top = unref(y)
const left = unref(x)
virtualElement.value.getBoundingClientRect = () => ({
width: 0,
height: 0,
top,
left
})
isContextMenuOpen.value = true
}
const defaultProps = {
Button: {
@@ -115,7 +156,9 @@ const defaultProps = {
title: 'A new software update is available. See whats new in version 2.0.4.'
},
AlertDialog: {
title: 'Are you sure you want to close this modal?',
icon: 'i-heroicons-exclamation-triangle',
title: 'Deactivate account',
description: 'Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.',
modelValue: alertDialog,
'onUpdate:modelValue': (v) => { alertDialog.value = v },
component: {
@@ -137,21 +180,21 @@ const defaultProps = {
items: [
[{
label: 'Edit',
icon: 'heroicons-solid:pencil'
icon: 'i-heroicons-pencil-square-20-solid'
}, {
label: 'Duplicate',
icon: 'heroicons-solid:duplicate'
icon: 'i-heroicons-document-duplicate-20-solid'
}],
[{
label: 'Archive',
icon: 'heroicons-solid:archive'
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'heroicons-solid:external-link'
icon: 'i-heroicons-arrow-right-circle-20-solid'
}],
[{
label: 'Delete',
icon: 'heroicons-solid:trash'
icon: 'i-heroicons-trash-20-solid'
}]
]
},
@@ -159,29 +202,38 @@ const defaultProps = {
links: [
{
label: 'Home',
icon: 'heroicons-outline:home',
icon: 'i-heroicons-home',
to: '/'
},
{
label: 'Examples',
icon: 'heroicons-outline:book-open',
icon: 'i-heroicons-book-open',
to: '/examples'
},
{
label: 'Migration',
icon: 'heroicons-outline:refresh',
icon: 'i-heroicons-arrow-path',
to: '/migration'
},
{
label: 'External link',
icon: 'heroicons-outline:external-link',
icon: 'i-heroicons-link',
to: 'https://google.fr',
target: '_blank'
}
]
},
CommandPalette: {
modelValue: commandPalette,
'onUpdate:modelValue': (v) => { commandPalette.value = v },
groups: [{
key: 'people',
label: 'People',
commands: people
}]
},
Icon: {
name: 'heroicons-outline:bell'
name: 'i-heroicons-bell'
},
Input: {
name: 'input',
@@ -192,10 +244,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 +268,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'
},
@@ -225,6 +285,31 @@ const defaultProps = {
title: 'Notification title',
callback: 'console.log(\'Timer expired\')'
},
ContextMenu: {
modelValue: isContextMenuOpen,
'onUpdate:modelValue': (v) => { isContextMenuOpen.value = v },
virtualElement,
component: {
name: 'Card',
props: {
variant: 'secondary',
label: 'Open context menu',
onClick: () => { isContextMenuOpen.value = false },
onContextmenu: (e) => {
e?.preventDefault()
openContextMenu()
},
class: 'relative w-[300px] h-[100px]'
}
},
slots: {
default: {
tag: 'div',
html: 'Context menu content',
class: 'rounded border u-border-gray-200 p-2'
}
}
},
Modal: {
modelValue: modal,
'onUpdate:modelValue': (v) => { modal.value = v },
@@ -242,14 +327,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 +363,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 +422,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 +430,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 +448,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 +485,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(() => {

View File

@@ -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>

View File

@@ -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">
@@ -25,7 +26,7 @@
Button:
</div>
<UButton variant="primary" icon="heroicons-outline:mail">
<UButton variant="primary" icon="i-heroicons-envelope">
Button text
</UButton>
</div>
@@ -39,10 +40,10 @@
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" />
<UIcon name="i-heroicons-exclamation" class="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium u-text-gray-900">
@@ -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">
@@ -72,7 +73,7 @@
</div>
<UDropdown v-slot="{ open }" :items="dropdownItems" placement="bottom-start">
<UButton variant="white" :icon="open ? 'heroicons-solid:chevron-up' : 'heroicons-solid:chevron-down'" trailing icon-class="transition">
<UButton variant="white" :icon="open ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
Open menu!
</UButton>
</UDropdown>
@@ -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,54 +96,16 @@
Popover:
</div>
<UPopover panel-class="w-screen max-w-sm px-4 sm:px-0 transform" wrapper-class="inline-block relative">
<UPopover 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">
<UButton variant="secondary" :icon="open ? 'i-heroicons-chevron-up-20-solid-20' : 'i-heroicons-chevron-down-20-solid'" trailing icon-class="transition">
Open popover!
</UButton>
</template>
<template #panel>
<div
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
>
<div class="relative grid gap-8 bg-white p-7">
<a
v-for="item in solutions"
:key="item.name"
:href="item.href"
class="flex items-center p-2 -m-3 transition duration-150 ease-in-out rounded-lg hover:bg-gray-50 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
>
<div
class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white sm:h-12 sm:w-12"
>
<div v-html="item.icon" />
</div>
<div class="ml-4">
<p class="text-sm font-medium u-text-gray-900">
{{ item.name }}
</p>
<p class="text-sm u-text-gray-500">
{{ item.description }}
</p>
</div>
</a>
</div>
<div class="p-4 bg-gray-50">
<a
href="##"
class="flow-root px-2 py-2 transition duration-150 ease-in-out rounded-md hover:bg-gray-100 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
>
<span class="flex items-center">
<span class="text-sm font-medium u-text-gray-900">
Documentation
</span>
</span>
<span class="block text-sm u-text-gray-500">
Start integrating products and tools
</span>
</a>
</div>
<div class="p-2">
Panel
</div>
</template>
</UPopover>
@@ -158,8 +116,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="i-heroicons-information-circle" class="w-6 h-6 u-text-gray-900 cursor-pointer" />
</UTooltip>
</div>
@@ -167,7 +125,40 @@
<div class="font-medium text-sm mb-1 u-text-gray-700">
Notifications:
</div>
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
<UButton icon="i-heroicons-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 class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="openContextMenu">
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
<div class="p-2">
Menu
</div>
</UContextMenu>
</UCard>
</div>
<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Command palette:
</div>
<UCard body-class="">
<UCommandPalette
:placeholder="false"
:options="{
fuseOptions: {
includeMatches: true
}
}"
:groups="commandPaletteGroups"
command-attribute="name"
/>
</UCard>
</div>
<div>
@@ -175,18 +166,34 @@
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="i-heroicons-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="i-heroicons-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" />
<UToggle v-model="form.toggle" name="toggle" icon-off="i-heroicons-x-mark-20-solid" icon-on="i-heroicons-check-20-solid" />
</UFormGroup>
<UFormGroup label="Notifications" label-class="text-base font-medium u-text-gray-900" description="How do you prefer to receive notifications?">
@@ -223,17 +230,66 @@
<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 virtualElement = ref({ getBoundingClientRect: () => ({}) })
const commandPaletteGroups = computed(() => ([{
key: 'people',
commands: people.value
}, {
key: 'search',
label: q => q && `Search results for "${q}"...`,
search: async (q) => {
if (!q) { return [] }
return await $fetch(`https://jsonplaceholder.typicode.com/users?q=${q}`)
}
}]))
onMounted(() => {
document.addEventListener('mousemove', ({ clientX, clientY }) => {
x.value = clientX
y.value = clientY
})
})
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
}
@@ -251,129 +307,44 @@ function onSubmit () {
const dropdownItems = [
[{
label: 'Edit',
icon: 'heroicons-solid:pencil',
click: () => onClick()
icon: 'i-heroicons-pencil-square-20-solid',
click: (e) => {
e.preventDefault()
onClick()
}
}, {
label: 'Duplicate',
icon: 'heroicons-solid:duplicate'
icon: 'i-heroicons-document-duplicate-20-solid'
}],
[{
label: 'Archive',
icon: 'heroicons-solid:archive'
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'heroicons-solid:external-link'
icon: 'i-heroicons-arrow-right-circle-20-solid',
to: 'https://www.google.fr',
target: '_blank'
}],
[{
label: 'Delete',
icon: 'heroicons-solid:trash'
icon: 'i-heroicons-trash-20-solid'
}]
]
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',
icon: 'heroicons-solid:plus',
icon: 'i-heroicons-plus-20-solid',
to: '/about'
}]
]
const solutions = [
{
name: 'Insights',
description: 'Measure actions your users take',
href: '##',
icon: `
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
aria-hidden='true'
xmlns="http://www.w3.org/2000/svg"
>
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
<path
d="M24 11L35.2583 17.5V30.5L24 37L12.7417 30.5V17.5L24 11Z"
stroke="#FB923C"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.7417 19.8094V28.1906L24 32.3812L31.2584 28.1906V19.8094L24 15.6188L16.7417 19.8094Z"
stroke="#FDBA74"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.7417 22.1196V25.882L24 27.7632L27.2584 25.882V22.1196L24 20.2384L20.7417 22.1196Z"
stroke="#FDBA74"
stroke-width="2"
/>
</svg>
`
},
{
name: 'Automations',
description: 'Create your own targeted content',
href: '##',
icon: `
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
<path
d="M28.0413 20L23.9998 13L19.9585 20M32.0828 27.0001L36.1242 34H28.0415M19.9585 34H11.8755L15.9171 27"
stroke="#FB923C"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.804 30H29.1963L24.0001 21L18.804 30Z"
stroke="#FDBA74"
stroke-width="2"
/>
</svg>
`
},
{
name: 'Reports',
description: 'Keep track of your growth',
href: '##',
icon: `
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
<rect x="13" y="32" width="2" height="4" fill="#FDBA74" />
<rect x="17" y="28" width="2" height="8" fill="#FDBA74" />
<rect x="21" y="24" width="2" height="12" fill="#FDBA74" />
<rect x="25" y="20" width="2" height="16" fill="#FDBA74" />
<rect x="29" y="16" width="2" height="20" fill="#FB923C" />
<rect x="33" y="12" width="2" height="24" fill="#FB923C" />
</svg>
`
}
]
const onNotificationClick = () => {
$toast.error({ title: 'Error', description: 'This is an error message' })
}

View File

@@ -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: {

View File

@@ -68,167 +68,218 @@ 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
},
{
label: 'Icon',
to: '/components/Icon',
nuxt3: true,
capi: true
capi: true,
preset: 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@nuxthq/ui",
"version": "0.0.2",
"version": "1.1.3",
"repository": "https://github.com/nuxtlabs/ui",
"license": "MIT",
"exports": {
@@ -18,37 +18,41 @@
"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",
"@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "^1.7.11",
"@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.2.2",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.4.1",
"@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.9",
"@vueuse/core": "^9.13.0",
"@vueuse/integrations": "^9.13.0",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"nanoid": "^3.2.0",
"pathe": "^0.2.0"
"tailwindcss": "^3.2.7"
},
"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"
},
"build": {
"externals": [
"@unocss/preset-uno"
]
"@iconify-json/mdi": "^1.1.47",
"@nuxt/module-builder": "^0.2.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.14.2",
"eslint": "^8.35.0",
"nuxt": "^3.2.2",
"standard-version": "^9.5.0",
"unbuild": "^1.1.2",
"vue-tsc": "^1.2.0"
}
}

6
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import type { DefaultPreset } from './runtime/presets/default'
declare module '#build/ui' {
declare const preset: DefaultPreset
export default preset
}

View File

@@ -1,8 +1,22 @@
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 { iconsPlugin, getIconCollections } from '@egoist/tailwindcss-icons'
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 +25,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 +40,123 @@ export interface ModuleOptions {
colors?: ColorsOptions
unocss?: UnocssNuxtOptions
icons: string[]
tailwindcss?: Partial<Config>
}
const defaults = {
preset: 'tailwindui',
preset: {},
prefix: 'u',
colors: {
primary: 'indigo',
gray: 'zinc'
gray: 'gray'
},
unocss: {
shortcuts: [],
rules: [],
variants: [],
icons: ['heroicons'],
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')
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']
}])
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
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 +189,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'))
}
})

View File

@@ -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>

View File

@@ -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 Object.keys($ui.avatar.size).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>

View File

@@ -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>

View File

@@ -6,180 +6,222 @@
:aria-label="ariaLabel"
v-bind="buttonProps"
>
<Icon v-if="isLeading" :name="iconName" :class="iconClass" 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="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
<slot>
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
</span>
</slot>
<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
},
labelCompact: {
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
},
compact: {
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' : (props.compact ? 'compact' : '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[props.compact ? 'compactSpacing' : '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[props.compact ? 'compactSpacing' : 'spacing'][props.size],
props.trailingIconClass,
props.loading && !isLeading.value && 'animate-spin'
)
})
</script>
<script lang="ts">
export default { name: 'UButton' }
</script>

View File

@@ -1,29 +1,40 @@
<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"
:disabled="disabled"
class="inline-flex w-full"
role="button"
@mouseover="onMouseOver"
>
<slot :open="open">
<button>Open</button>
<button :disabled="disabled">
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 && items.length" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<transition appear v-bind="transitionClass">
<MenuItems :class="[baseClass, divideClass, ringClass, roundedClass, shadowClass, backgroundClass]" 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: itemDisabled }" :disabled="item.disabled">
<Component
v-bind="omit(item, ['click'])"
:is="(item.to && NuxtLink) || (item.click && 'button') || 'div'"
:class="resolveItemClass({ active, disabled: itemDisabled })"
@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 +45,210 @@
</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
}
disabled: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.dropdown.wrapper
},
containerClass: {
type: String,
default: () => $ui.dropdown.container
},
widthClass: {
type: String,
default: () => $ui.dropdown.width
},
backgroundClass: {
type: String,
default: () => $ui.dropdown.background
},
shadowClass: {
type: String,
default: () => $ui.dropdown.shadow
},
roundedClass: {
type: String,
default: () => $ui.dropdown.rounded
},
ringClass: {
type: String,
default: () => $ui.dropdown.ring
},
divideClass: {
type: String,
default: () => $ui.dropdown.divide
},
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>

View File

@@ -1,14 +1,16 @@
<template>
<div :class="name" />
<span :class="name" />
</template>
<script>
export default {
props: {
name: {
type: String,
required: true
}
<script setup lang="ts">
defineProps({
name: {
type: String,
required: true
}
}
})
</script>
<script lang="ts">
export default { name: 'UIcon' }
</script>

View File

@@ -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>

View File

@@ -1,103 +1,106 @@
<template>
<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" />
<div class="inline-flex flex-shrink-0">
<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 }} &rarr;
</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: 'i-heroicons-information-circle',
warning: 'i-heroicons-exclamation',
error: 'i-heroicons-x-circle',
success: 'i-heroicons-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>

View File

@@ -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 Icon from '../elements/Icon.vue'
import Modal from '../overlays/Modal.vue'
import Button from '../elements/Button.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: ''
@@ -51,7 +55,7 @@ const props = defineProps({
},
titleClass: {
type: String,
default: () => $ui.alertDialog.icon.title
default: () => $ui.alertDialog.title
},
description: {
type: String,
@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 { classNames } from '../../utils'
import Icon from '../elements/Icon.vue'
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>

View File

@@ -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" aria-hidden="true">
<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" role="button" 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 }[] | string[]>,
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 as any[]).filter((option: any) => {
return (props.searchAttributes?.length ? props.searchAttributes : [props.textAttribute]).some((searchAttribute: any) => {
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,269 @@
<template>
<Combobox
ref="comboboxRef"
:by="by"
:model-value="modelValue"
:multiple="multiple"
:nullable="nullable"
@update:model-value="onSelect"
>
<div :class="$ui.commandPalette.wrapper">
<div v-show="searchable" class="relative flex items-center">
<Icon v-if="inputIcon" :name="inputIcon" :class="$ui.commandPalette.input.icon.base" aria-hidden="true" />
<ComboboxInput
ref="comboboxInput"
:value="query"
:class="$ui.commandPalette.input.base"
:placeholder="inputPlaceholder"
autocomplete="off"
@change="query = $event.target.value"
/>
<Button
v-if="inputCloseIcon"
:icon="inputCloseIcon"
:class="$ui.commandPalette.input.close.base"
:size="$ui.commandPalette.input.close.size"
:variant="$ui.commandPalette.input.close.variant"
aria-label="Close"
@click="onClear"
/>
</div>
<ComboboxOptions
v-if="groups.length"
static
hold
as="div"
aria-label="Commands"
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"
:query="query"
: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 v-if="emptyIcon" :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400 mb-4" aria-hidden="true" />
<p class="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 { useDebounceFn } from '@vueuse/core'
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'
import $ui from '#build/ui'
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: () => []
},
inputIcon: {
type: String,
default: () => $ui.commandPalette.input.icon.name
},
inputCloseIcon: {
type: String,
default: () => $ui.commandPalette.input.close.icon.name
},
inputPlaceholder: {
type: String,
default: 'Search...'
},
emptyIcon: {
type: String,
default: () => $ui.commandPalette.empty.icon.name
},
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
},
debounce: {
type: Number,
default: 200
}
})
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.filter(group => !group.search).reduce((acc, group) => {
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
}, [] as Command[]))
const searchResults = ref<{ [key: string]: any }>({})
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
}),
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
]))
const debouncedSearch = useDebounceFn(async () => {
const searchableGroups = props.groups.filter(group => !!group.search)
await Promise.all(searchableGroups.map(async (group) => {
searchResults.value[group.key] = await group.search(query.value)
}))
}, props.debounce)
watch(query, () => {
debouncedSearch()
// 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({
query,
updateQuery: (q: string) => {
query.value = q
},
comboboxApi,
results
})
</script>
<script lang="ts">
export default { name: 'UCommandPalette' }
</script>

View File

@@ -0,0 +1,102 @@
<template>
<div class="p-2" role="option">
<h2 v-if="label" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
{{ label }}
</h2>
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
<ComboboxOption
v-for="(command, index) of group.commands"
:key="`${group.key}-${index}`"
v-slot="{ active, selected }"
:value="command"
:disabled="command.disabled"
as="template"
>
<div :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">
<slot :name="`${group.key}-icon`" :group="group" :command="command">
<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"
aria-hidden="true"
/>
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
</slot>
<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="$ui.commandPalette.option.selected.icon.name" 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>
</div>
</ComboboxOption>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
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'
import $ui from '#build/ui'
const props = defineProps({
group: {
type: Object as PropType<Group>,
required: true
},
query: {
type: String,
default: ''
},
groupAttribute: {
type: String,
required: true
},
commandAttribute: {
type: String,
required: true
}
})
const label = computed(() => {
const label = props.group[props.groupAttribute]
return typeof label === 'function' ? label(props.query) : label
})
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 | string
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>

View File

@@ -0,0 +1,91 @@
<template>
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
<transition appear v-bind="transitionClass">
<div :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]">
<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
},
backgroundClass: {
type: String,
default: () => $ui.contextMenu.background
},
shadowClass: {
type: String,
default: () => $ui.contextMenu.shadow
},
roundedClass: {
type: String,
default: () => $ui.contextMenu.rounded
},
ringClass: {
type: String,
default: () => $ui.contextMenu.ring
},
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>

View File

@@ -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">&#8203;</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>

View File

@@ -1,48 +1,43 @@
<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"
class="inline-flex 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"
>
<span class="sr-only">Close</span>
<Icon name="heroicons-solid:x" class="w-5 h-5" />
<Icon :name="$ui.notification.close.icon.name" class="w-5 h-5" />
</button>
</div>
</div>
@@ -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>

View File

@@ -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>

View File

@@ -1,91 +1,164 @@
<template>
<Popover v-slot="{ open }" :class="wrapperClass">
<PopoverButton ref="trigger" as="div">
<slot :open="open">
<button>Open</button>
<Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave">
<PopoverButton
ref="trigger"
as="div"
:disabled="disabled"
class="inline-flex w-full"
role="button"
@mouseover="onMouseOver"
>
<slot :open="open" :close="close">
<button :disabled="disabled">
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, ringClass, roundedClass, shadowClass, backgroundClass]" 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
}
disabled: {
type: Boolean,
default: false
},
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
},
backgroundClass: {
type: String,
default: () => $ui.popover.background
},
shadowClass: {
type: String,
default: () => $ui.popover.shadow
},
roundedClass: {
type: String,
default: () => $ui.popover.rounded
},
ringClass: {
type: String,
default: () => $ui.popover.ring
},
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>

View File

@@ -0,0 +1,141 @@
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="close">
<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', 'close'])
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
}
})
function close (value: boolean) {
isOpen.value = value
emit('close')
}
</script>
<script lang="ts">
export default { name: 'USlideover' }
</script>

View File

@@ -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 }}
</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">&middot;</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>

View 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
}

View File

@@ -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()
}

View File

@@ -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
}

View 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
}
}
}
})

View File

@@ -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
}
}
})

View File

@@ -0,0 +1,648 @@
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'
},
compact: {
xxs: 'p-1 sm:px-2',
xs: 'p-1.5 sm:px-2.5',
sm: 'p-2 sm:px-3',
md: 'p-2 sm:px-4',
lg: 'p-2 sm:px-4',
xl: 'p-3 sm:px-6'
},
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: 'i-heroicons-arrow-path',
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'
},
compactSpacing: {
xxs: 'sm:-ml-0.5 sm:mr-1',
xs: 'sm:-ml-0.5 sm:mr-1.5',
sm: 'sm:-ml-0.5 sm:mr-2',
md: 'sm:-ml-1 sm:mr-2',
lg: 'sm:-ml-1 sm:mr-3',
xl: 'sm:-ml-1 sm: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'
},
compactSpacing: {
xxs: 'sm:ml-1 sm:-mr-0.5',
xs: 'sm:ml-1.5 sm:-mr-0.5',
sm: 'sm:ml-2 sm:-mr-0.5',
md: 'sm:ml-2 sm:-mr-1',
lg: 'sm:ml-3 sm:-mr-1',
xl: 'sm:ml-3 sm:-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: 'i-heroicons-arrow-path',
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: 'i-heroicons-chevron-up-down-20-solid',
...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: 'i-heroicons-check-20-solid',
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: 'mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-primary-100 sm:mx-0 sm:h-10 sm:w-10',
base: 'h-6 w-6 text-primary-600'
},
title: 'text-lg leading-6 font-medium u-text-gray-900',
description: 'text-sm u-text-gray-500'
}
const dropdown = {
wrapper: 'relative inline-flex text-left',
container: 'z-20',
width: 'w-48',
background: 'u-bg-white',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 u-ring-gray-200',
base: 'focus:outline-none',
divide: 'divide-y u-divide-gray-100',
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: 'i-heroicons-information-circle',
success: 'i-heroicons-check-circle',
warning: 'i-heroicons-exclamation-circle',
error: 'i-heroicons-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'
}
},
close: {
icon: {
name: 'i-heroicons-x-mark-20-solid'
}
},
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 truncate',
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: '',
background: 'u-bg-white',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 u-ring-gray-200',
base: 'overflow-hidden focus:outline-none',
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: '',
background: 'u-bg-white',
shadow: 'shadow-lg',
rounded: 'rounded-md',
ring: 'ring-1 u-ring-gray-200',
base: 'overflow-hidden focus:outline-none',
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
}
}
const commandPalette = {
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
input: {
base: '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',
icon: {
base: 'pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400',
name: 'i-heroicons-magnifying-glass'
},
close: {
base: 'absolute right-2',
variant: 'transparent',
size: 'md',
icon: {
name: ''
}
}
},
empty: {
icon: {
name: 'i-heroicons-magnifying-glass'
}
},
option: {
selected: {
icon: {
name: 'i-heroicons-check-20-solid'
}
}
}
}
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,
commandPalette
}
}
export type DefaultPreset = ReturnType<typeof defaultPreset>

View File

@@ -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
View 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
View 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
View 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
View 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[]
[key: string]: any
}

5
src/runtime/types/index.d.ts vendored Normal file
View 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
View 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
}

View File

@@ -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
}

View 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('|')

View File

@@ -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))
)
}

View File

@@ -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]
}

View File

@@ -1,23 +1,3 @@
{
"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"]
}
}
"extends": "./docs/.nuxt/tsconfig.json"
}

7884
yarn.lock

File diff suppressed because it is too large Load Diff