Compare commits

..

89 Commits

Author SHA1 Message Date
Benjamin Canac
60eea0e46b chore(release): 2.3.0 2023-06-05 12:12:48 +02:00
Benjamin Canac
5e50eb9eb8 fix: use cloneVNode when altering props in render functions
Resolves #252

https://vuejs.org/api/render-function.html#clonevnode
2023-06-05 11:17:31 +02:00
Dominik Opyd
af65683123 docs(github): support for various file extensions (#250) 2023-06-02 11:35:31 +02:00
Benjamin Canac
2c673f5377 fix(CommandPalette): override of closeButton and emptyState props 2023-06-01 17:15:04 +02:00
Benjamin Canac
192b0e6301 fix(Table): override of sortButton and emptyState props 2023-06-01 17:14:45 +02:00
Benjamin Canac
71edb91c4f fix(Avatar): placeholder font size 2023-06-01 16:47:23 +02:00
Benjamin Canac
f9b935f5f5 fix(Badge): remove console.log in validator 2023-06-01 16:24:19 +02:00
Benjamin Canac
23833e92cb chore(Badge): handle color override like buttons 2023-06-01 16:06:07 +02:00
Benjamin Canac
241df7f05e docs: fix toc scroll when duplicated names 2023-06-01 15:41:23 +02:00
Benjamin Canac
130a1f2c54 docs: improve slots sections 2023-06-01 15:29:02 +02:00
Benjamin Canac
c63981e31c docs: :component-card now handle slots 2023-06-01 15:28:39 +02:00
Benjamin Canac
687f0c6f63 docs: improve inline code blocks inside Alert component 2023-06-01 15:28:12 +02:00
Benjamin Canac
f59a92ca15 chore(Input)!: move pointer class inside its own preset class 2023-06-01 15:27:53 +02:00
Benjamin Canac
01fa85c7a3 fix(defineShortcuts): err with input autocomplete that triggers keydown 2023-06-01 15:26:46 +02:00
Benjamin Canac
3434bc7f2b chore(deps): bump @nuxthq/studio 2023-06-01 11:43:47 +02:00
Benjamin Canac
9b1aacb1da docs: move slots sections as h2 2023-06-01 11:39:07 +02:00
Benjamin Canac
8951923a11 fix(SelectMenu): disable on loading 2023-06-01 11:08:16 +02:00
Benjamin Canac
e200d4cc74 chore(package): remove preinstall script 2023-06-01 10:49:36 +02:00
Benjamin Canac
e05619f8c8 chore: add leading and trailing slots
Resolves #246
2023-05-31 23:53:31 +02:00
Benjamin Canac
5ea43ab4e4 chore: uniformize icons in Button / Input / Select / SelectMenu
Also adds `loading` to `Select` and `SelectMenu`
2023-05-31 23:30:52 +02:00
Benjamin Canac
ba44c58a80 chore(SelectMenu)!: remove inline-flex from wrapper to behave like other form elements 2023-05-31 23:22:58 +02:00
Benjamin Canac
490025a981 docs: add Table icons in theming icons section 2023-05-31 18:31:15 +02:00
Benjamin Canac
2966373a86 chore(Table): handle empty-state
Resolves #243
2023-05-31 18:30:49 +02:00
Sylvain Marroufin
8bdb8c45f7 chore(Dropdown): hover mode with padding instead of offset + improve docs (#242)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-31 14:28:14 +02:00
Sylvain Marroufin
9827de0b58 docs(textarea): improve props documentation (#241) 2023-05-31 12:49:47 +02:00
Benjamin Canac
23f01fde41 chore(Table): rename preset container to base 2023-05-31 12:11:28 +02:00
Benjamin Canac
f680318e44 docs: fix overflow in table page
Fixes #244
2023-05-31 12:11:12 +02:00
Benjamin Canac
cd2d1eb1fa docs(table): add alert on sort prop without sortable 2023-05-30 18:06:03 +02:00
Benjamin Canac
3ba0aedcba fix(Table): type sort prop 2023-05-30 18:03:24 +02:00
Benjamin Canac
40b6884424 chore(Table): handle default sort and default column direction 2023-05-30 17:59:30 +02:00
Benjamin Canac
a2638c6057 chore(Table): split container divide 2023-05-30 16:23:17 +02:00
Benjamin Canac
6bd5142a37 fix(Table): add missing text-left in th.base 2023-05-30 16:21:54 +02:00
Benjamin Canac
bc1d653857 chore(Table): split preset for th and td 2023-05-30 16:17:17 +02:00
Benjamin Canac
6c215e07a6 chore(deps): bump 2023-05-30 12:18:05 +02:00
Benjamin Canac
272af9d24c fix(Table): missing ref import from vue 2023-05-30 12:17:59 +02:00
Benjamin Canac
cce000ab2b feat: add Table component (#237) 2023-05-30 12:13:57 +02:00
Benjamin Canac
4a99d6a7bb docs: fix notification preset for closeButton and actionButton 2023-05-29 22:45:33 +02:00
Benjamin Canac
4458656be5 chore(Notification)!: rename to closeButton and actionButton for consistency 2023-05-29 21:59:59 +02:00
Benjamin Canac
daca46371c chore(CommandPalette)!: rename props to emptyState and closeButton for consistency 2023-05-29 21:55:08 +02:00
Benjamin Canac
8ee2ac10e7 chore(Toggle)!: rename icons to onIcon / offIcon for consistency 2023-05-29 21:38:51 +02:00
Benjamin Canac
1ebaa5aa00 fix(Button): invalid padding when using square prop 2023-05-29 21:36:28 +02:00
Benjamin Canac
cb43548305 chore(SelectMenu): handle multiple default display + specific placeholder 2023-05-29 11:52:02 +02:00
Benjamin Canac
360084af7c chore(Toggle): improve component
- allow `iconOn` / `iconOff` default values from preset
- `bg-gray-900` on dark mode inside of `bg-white`
- added `name` prop for form control
2023-05-27 22:27:31 +02:00
Benjamin Canac
0af5184c70 chore(release): 2.2.1 2023-05-27 12:27:53 +02:00
Benjamin Canac
44c3e2c46a chore(forms): remove required on Input, Select and Textarea name
Resolves #236
2023-05-27 12:03:29 +02:00
Benjamin Canac
a96dc19215 fix(FormGroup): missing h import from vue
Resolves #236
2023-05-27 12:02:51 +02:00
Benjamin Canac
aa881a8d00 chore(release): 2.2.0 2023-05-26 23:19:53 +02:00
Benjamin Canac
08413f198b scripts: update to pnpm 2023-05-26 22:46:17 +02:00
Benjamin Canac
75ab1d2ed5 chore(deps): bump 2023-05-26 22:25:58 +02:00
Sumit Kolhe
2d6ce654f4 docs: add close button to Slideover example (#211)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2023-05-26 22:20:34 +02:00
Benjamin Canac
9ce531a06f feat!: handle color states on form elements (#234) 2023-05-26 22:07:49 +02:00
Benjamin Canac
1a9dc5c980 fix(Notification): remove default color on icon 2023-05-26 18:28:52 +02:00
Benjamin Canac
589f86ef1b chore(Avatar): dark variant for chip background color 2023-05-26 18:28:32 +02:00
Benjamin Canac
1b61ec72e2 chore(Notification)!: rename progressColor to color and style icon
This also removes `progressVariant` prop
2023-05-26 18:03:54 +02:00
Benjamin Canac
1f22f84360 chore(Avatar)!: remove chipVariant prop 2023-05-26 18:02:48 +02:00
Benjamin Canac
2c6db975f9 chore(deps): switch to pnpm (#228) 2023-05-26 17:41:07 +02:00
Benjamin Canac
b7099aa0d3 chore(SelectMenu): add searchablePlaceholder prop
Resolves #231
2023-05-26 15:02:21 +02:00
Benjamin Canac
36b0869bc2 docs: fix prev card gap on first page 2023-05-23 16:57:42 +02:00
Benjamin Canac
28167e41ff docs: add VerticalNavigation tailwind example 2023-05-23 15:27:13 +02:00
Benjamin Canac
19923cbf1e chore(VerticalNavigation)!: split preset 2023-05-23 15:26:47 +02:00
Benjamin Canac
1210e99ec1 chore(VerticalNavigation): improve types import 2023-05-23 15:25:28 +02:00
Benjamin Canac
fc894bc1ae chore(Dropdown): improve types import 2023-05-23 15:25:12 +02:00
Benjamin Canac
9491ac7172 chore(CommandPalette): improve types import 2023-05-23 15:25:00 +02:00
Benjamin Canac
32dc2264d8 chore(types): export button 2023-05-23 15:24:41 +02:00
Benjamin Canac
45ba3b26da chore(Notification): improve types 2023-05-23 15:24:32 +02:00
Benjamin Canac
6d3309c42d chore(Notification): move padding to app.config 2023-05-23 11:25:56 +02:00
Benjamin Canac
530b85136d docs: handle color mode in volta embed 2023-05-23 11:11:19 +02:00
Benjamin Canac
cb9ed9ad3f chore(Button): inject NuxtLink in components 2023-05-22 19:05:39 +02:00
Benjamin Canac
524e220914 chore(VerticalNavigation): improve binds & types 2023-05-22 19:05:17 +02:00
Benjamin Canac
e3e6ef27a2 docs: improve Dropdown example with click and disabled 2023-05-22 19:04:39 +02:00
Benjamin Canac
55f115f9fe chore(Dropdown): use ULinkCustom + improve item binds & types
Fixes #215
2023-05-22 19:04:18 +02:00
Benjamin Canac
bdaf2dbbd4 chore(CommandPalette): handle loading state (#221) 2023-05-22 16:00:31 +02:00
Benjamin Canac
e7eea067b2 chore(Notification): add progressColor and progressVariant props (#219)
Co-authored-by: Sébastien Chopin <seb@nuxtjs.com>
2023-05-22 15:01:19 +02:00
Benjamin Canac
a56dbeab35 fix(Radio/Checkbox): remove ring offset on focus 2023-05-22 13:41:56 +02:00
Benjamin Canac
570b82d1e7 chore(Avatar): allow default value for chipColor through app.config.ts 2023-05-22 12:24:17 +02:00
Harry Yep
b5189c0c07 docs: LogoLabs not shown (#216) 2023-05-21 23:01:08 +02:00
Sébastien Chopin
8a0a5d8ba0 docs: pre-render component-meta routes 2023-05-20 19:13:58 +02:00
Sébastien Chopin
d3e5f4e15d docs: remove console.log 2023-05-20 18:53:32 +02:00
Sébastien Chopin
5a592b7ee0 docs: use CF rules for redirect 2023-05-20 18:49:39 +02:00
Sébastien Chopin
43787eca74 docs: move vercel.json to public dir 2023-05-20 18:41:07 +02:00
Sébastien Chopin
595ed9fb46 docs: add vercel redirect 2023-05-20 18:38:19 +02:00
Sébastien Chopin
5c4ab26d25 docs: support ssg 2023-05-20 18:31:56 +02:00
Sébastien Chopin
2030f24a47 docs: update logo on aside on mobile 2023-05-20 13:18:19 +02:00
Benjamin Canac
6eda322496 chore(VerticalNavigation): links badge type as number
Resolves #206
2023-05-19 15:55:18 +02:00
Benjamin Canac
318f8b2f08 docs: improve theming colors section 2023-05-19 15:00:39 +02:00
Benjamin Canac
dfab900562 docs: add badge in VerticalNavigation example 2023-05-19 14:51:31 +02:00
Benjamin Canac
d2ee5058f8 fix(VerticalNavigation): badge display
Resolves #205
2023-05-19 14:51:16 +02:00
Benjamin Canac
e358183165 docs: getting started title on index 2023-05-19 13:01:34 +02:00
Benjamin Canac
26579538f5 docs: prevent Alert text hover without link 2023-05-19 13:00:50 +02:00
98 changed files with 13374 additions and 10464 deletions

View File

@@ -22,30 +22,41 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Checkout - name: checkout
uses: actions/checkout@master uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0
- name: Cache - uses: pnpm/action-setup@v2
uses: actions/cache@v3 name: Install pnpm
id: pnpm-install
with: with:
path: node_modules version: 7
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} run_install: false
- name: Dependencies - name: Get pnpm store directory
if: steps.cache.outputs.cache-hit != 'true' id: pnpm-cache
run: yarn shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint - name: Lint
run: yarn lint run: pnpm run lint
- name: Typecheck - name: Typecheck
run: yarn typecheck run: pnpm run typecheck
- name: Build - name: Build
run: yarn build run: pnpm run build
- name: Release Edge - name: Release Edge
if: github.event_name == 'push' if: github.event_name == 'push'

View File

@@ -22,30 +22,41 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Checkout - name: checkout
uses: actions/checkout@master uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0
- name: Cache - uses: pnpm/action-setup@v2
uses: actions/cache@v3 name: Install pnpm
id: pnpm-install
with: with:
path: node_modules version: 7
key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} run_install: false
- name: Dependencies - name: Get pnpm store directory
if: steps.cache.outputs.cache-hit != 'true' id: pnpm-cache
run: yarn shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint - name: Lint
run: yarn lint run: pnpm run lint
- name: Typecheck - name: Typecheck
run: yarn typecheck run: pnpm run typecheck
- name: Build - name: Build
run: yarn build run: pnpm run build
- name: Version Check - name: Version Check
id: check id: check

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ nuxt.d.ts
dist dist
.DS_Store .DS_Store
.history .history
.vercel

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

View File

@@ -2,6 +2,76 @@
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. 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.
## [2.3.0](https://github.com/nuxtlabs/ui/compare/v2.2.1...v2.3.0) (2023-06-05)
### ⚠ BREAKING CHANGES
* **Input:** move pointer class inside its own preset class
* **SelectMenu:** remove `inline-flex` from wrapper to behave like other form elements
* **Notification:** rename to `closeButton` and `actionButton` for consistency
* **CommandPalette:** rename props to `emptyState` and `closeButton` for consistency
* **Toggle:** rename icons to `onIcon` / `offIcon` for consistency
### Features
* add `Table` component ([#237](https://github.com/nuxtlabs/ui/issues/237)) ([cce000a](https://github.com/nuxtlabs/ui/commit/cce000ab2b2af1079216e0e79769703fc4d9933e))
### Bug Fixes
* **Avatar:** placeholder font size ([71edb91](https://github.com/nuxtlabs/ui/commit/71edb91c4ff17a258d6229ed6c6fa6a4b54bdd53))
* **Badge:** remove `console.log` in validator ([f9b935f](https://github.com/nuxtlabs/ui/commit/f9b935f5f59b872fd952a2739d305d6574bf7cf8))
* **Button:** invalid padding when using `square` prop ([1ebaa5a](https://github.com/nuxtlabs/ui/commit/1ebaa5aa00752cd276f7c754d64ac7f85b14dc26))
* **CommandPalette:** override of `closeButton` and `emptyState` props ([2c673f5](https://github.com/nuxtlabs/ui/commit/2c673f5377dbbcdefa6b57eddba2c19d065d5f1f))
* **defineShortcuts:** err with input autocomplete that triggers `keydown` ([01fa85c](https://github.com/nuxtlabs/ui/commit/01fa85c7a3e476d4f710ed3a36c1e815fc986a94))
* **SelectMenu:** disable on loading ([8951923](https://github.com/nuxtlabs/ui/commit/8951923a11d533ebf53dbec5f852800555af253c))
* **Table:** add missing `text-left` in `th.base` ([6bd5142](https://github.com/nuxtlabs/ui/commit/6bd5142a377694599952e0f9b53fde0d0132b61b))
* **Table:** missing `ref` import from `vue` ([272af9d](https://github.com/nuxtlabs/ui/commit/272af9d24c7cda8341e66b57f76acdb9f46ea23e))
* **Table:** override of `sortButton` and `emptyState` props ([192b0e6](https://github.com/nuxtlabs/ui/commit/192b0e63018ae73e8acaa8b4b1771cda2b59bdb6))
* **Table:** type `sort` prop ([3ba0aed](https://github.com/nuxtlabs/ui/commit/3ba0aedcba578350e2fdd9c180505ed8920e0404))
* use `cloneVNode` when altering props in render functions ([5e50eb9](https://github.com/nuxtlabs/ui/commit/5e50eb9eb82571d22e0a2f1a2fe985addf7efe18)), closes [#252](https://github.com/nuxtlabs/ui/issues/252)
* **CommandPalette:** rename props to `emptyState` and `closeButton` for consistency ([daca463](https://github.com/nuxtlabs/ui/commit/daca46371cab1344bd87ffb0abe0f7e9cdb08609))
* **Input:** move pointer class inside its own preset class ([f59a92c](https://github.com/nuxtlabs/ui/commit/f59a92ca1533a44e17fbc8b7945bdaa9a83e805a))
* **Notification:** rename to `closeButton` and `actionButton` for consistency ([4458656](https://github.com/nuxtlabs/ui/commit/4458656be5547fc9505a5c4758bea4818ada408b))
* **SelectMenu:** remove `inline-flex` from wrapper to behave like other form elements ([ba44c58](https://github.com/nuxtlabs/ui/commit/ba44c58a80252a4394fcf2f84611ea2696883120))
* **Toggle:** rename icons to `onIcon` / `offIcon` for consistency ([8ee2ac1](https://github.com/nuxtlabs/ui/commit/8ee2ac10e7eda4c54418f613a5ef87dd89e1f7eb))
### [2.2.1](https://github.com/nuxtlabs/ui/compare/v2.2.0...v2.2.1) (2023-05-27)
### Bug Fixes
* **FormGroup:** missing `h` import from `vue` ([a96dc19](https://github.com/nuxtlabs/ui/commit/a96dc192157725143503b1a5e4b404cb48dc9d3f)), closes [#236](https://github.com/nuxtlabs/ui/issues/236)
## [2.2.0](https://github.com/nuxtlabs/ui/compare/v2.1.0...v2.2.0) (2023-05-26)
### ⚠ BREAKING CHANGES
* handle color states on form elements (#234)
* **Notification:** rename `progressColor` to `color` and style icon
* **Avatar:** remove `chipVariant` prop
* **VerticalNavigation:** split preset
### Features
* handle color states on form elements ([#234](https://github.com/nuxtlabs/ui/issues/234)) ([9ce531a](https://github.com/nuxtlabs/ui/commit/9ce531a06f1a972bc003876162e0503c1bbbdbd8))
### Bug Fixes
* **Notification:** remove default color on icon ([1a9dc5c](https://github.com/nuxtlabs/ui/commit/1a9dc5c980d8477cdf9386a17e20fc9fec0d883e))
* **Radio/Checkbox:** remove ring offset on focus ([a56dbea](https://github.com/nuxtlabs/ui/commit/a56dbeab351a5c58e5bb49f5762669e2884c6483))
* **VerticalNavigation:** badge display ([d2ee505](https://github.com/nuxtlabs/ui/commit/d2ee5058f819fc17f281f323dab2f0b3d80cf7bd)), closes [#205](https://github.com/nuxtlabs/ui/issues/205)
* **Avatar:** remove `chipVariant` prop ([1f22f84](https://github.com/nuxtlabs/ui/commit/1f22f84360c20498eea8971b21db9293a4c9c3dc))
* **Notification:** rename `progressColor` to `color` and style icon ([1b61ec7](https://github.com/nuxtlabs/ui/commit/1b61ec72e292325d7776a4719f14a75bdb18e110))
* **VerticalNavigation:** split preset ([19923cb](https://github.com/nuxtlabs/ui/commit/19923cbf1edc6c6d4aefb9ffab9f908b116e1c69))
## [2.1.0](https://github.com/nuxtlabs/ui/compare/v2.0.4...v2.1.0) (2023-05-19) ## [2.1.0](https://github.com/nuxtlabs/ui/compare/v2.0.4...v2.1.0) (2023-05-19)

View File

@@ -3,7 +3,7 @@
<UContainer> <UContainer>
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white"> <NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" /> <Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
NuxtLabs<span class="text-primary-500 dark:text-primary-400">UI</span> NuxtLabs<span class="text-primary-500 dark:text-primary-400">UI</span>
@@ -62,10 +62,9 @@
<div class="px-4 sm:px-6 sticky top-0 border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10"> <div class="px-4 sm:px-6 sticky top-0 border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75 backdrop-blur z-10">
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-gray-900 dark:text-white"> <NuxtLink to="/getting-started" class="flex items-end gap-1.5 font-bold text-xl text-gray-900 dark:text-white">
<Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" /> <Logo class="w-8 h-8 text-primary-500 dark:text-primary-400" />
NuxtLabs<span class="text-primary-500 dark:text-primary-400">UI</span>
nuxthq/ui
</NuxtLink> </NuxtLink>
</div> </div>

View File

@@ -1,48 +1,52 @@
<template> <template>
<div class="flex items-center shadow-sm"> <div class="flex items-center shadow-sm">
<USelectMenu <ClientOnly>
v-model="primary" <USelectMenu
name="primary" v-model="primary"
class="w-full [&>div>button]:!rounded-r-none" name="primary"
appearance="gray" class="w-full [&>div>button]:!rounded-r-none"
:ui="{ width: 'w-[194px]' }" color="gray"
:popper="{ placement: 'bottom-start' }" :ui="{ width: 'w-[194px]' }"
:options="primaryOptions" :popper="{ placement: 'bottom-start' }"
> :options="primaryOptions"
<template #label> >
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" /> <template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${primary.hex}`}" />
{{ primary.text }} {{ primary.text }}
</template> </template>
<template #option="{ option }"> <template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" /> <span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }} {{ option.text }}
</template> </template>
</USelectMenu> </USelectMenu>
</ClientOnly>
<USelectMenu <ClientOnly>
v-model="gray" <USelectMenu
name="gray" v-model="gray"
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px" name="gray"
appearance="gray" class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
:ui="{ width: 'w-[194px]' }" color="gray"
:popper="{ placement: 'bottom-end' }" :ui="{ width: 'w-[194px]' }"
:options="grayOptions" :popper="{ placement: 'bottom-end' }"
> :options="grayOptions"
<template #label> >
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" /> <template #label>
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${gray.hex}`}" />
{{ gray.text }} {{ gray.text }}
</template> </template>
<template #option="{ option }"> <template #option="{ option }">
<span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" /> <span class="flex-shrink-0 h-3 w-3 rounded-full" :style="{ backgroundColor: `${option.hex}`}" />
{{ option.text }} {{ option.text }}
</template> </template>
</USelectMenu> </USelectMenu>
</ClientOnly>
</div> </div>
</template> </template>
@@ -84,4 +88,43 @@ const gray = computed({
grayCookie.value = option.value grayCookie.value = option.value
} }
}) })
// Hack for SSG
const hexToRgb = (hex) => {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
return r + r + g + g + b + b
})
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
: null
}
const root = computed(() => {
return `:root {
${Object.entries(colors[primary.value.value] || colors.green).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
${Object.entries(colors[gray.value.value] || colors.cool).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
}`
})
if (process.client) {
watch(root, () => {
window.localStorage.setItem('nuxt-ui-root', root.value)
}, { immediate: true })
}
if (process.server) {
useHead({
script: [
{
innerHTML: `
if (localStorage.getItem('nuxt-ui-root')) {
document.querySelector('style#nuxt-ui-colors').innerHTML = localStorage.getItem('nuxt-ui-root')
}`.replace(/\s+/g, ' '),
type: 'text/javascript',
tagPriority: -1
}
]
})
}
</script> </script>

View File

@@ -2,8 +2,8 @@
<component <component
:is="to ? NuxtLink : 'div'" :is="to ? NuxtLink : 'div'"
:to="to" :to="to"
class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-200 text-sm leading-6 my-5 last:mb-0 font-normal group relative" class="block pl-4 pr-6 py-3 rounded-md !border !border-gray-200 dark:!border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm leading-6 my-5 last:mb-0 font-normal group relative prose-code:bg-white dark:prose-code:bg-gray-900"
:class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed' : '']" :class="[to ? 'hover:!border-primary-500 dark:hover:!border-primary-400 hover:text-primary-500 dark:hover:text-primary-400 border-dashed hover:text-gray-800 dark:hover:text-gray-200' : '']"
> >
<UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" /> <UIcon v-if="!!to" name="i-heroicons-link-20-solid" class="w-3 h-3 absolute right-2 top-2 text-gray-400 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400" />

View File

@@ -2,21 +2,21 @@
<div> <div>
<div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose"> <div v-if="propsToSelect.length" class="relative flex border border-gray-200 dark:border-gray-700 rounded-t-md overflow-hidden not-prose">
<div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700"> <div v-for="prop in propsToSelect" :key="prop.name" class="flex flex-col gap-0.5 justify-between py-1.5 font-medium bg-gray-50 dark:bg-gray-800 border-r border-r-gray-200 dark:border-r-gray-700">
<label :for="prop.name" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label> <label :for="`prop-${prop.name}`" class="block text-xs px-3 font-medium text-gray-400 dark:text-gray-500 -my-px">{{ prop.label }}</label>
<UCheckbox <UCheckbox
v-if="prop.type === 'boolean'" v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]" v-model="componentProps[prop.name]"
:name="prop.name" :name="`prop-${prop.name}`"
appearance="none" variant="none"
class="justify-center" class="justify-center"
/> />
<USelectMenu <USelectMenu
v-else-if="prop.type === 'string' && prop.options.length" v-else-if="prop.type === 'string' && prop.options.length"
v-model="componentProps[prop.name]" v-model="componentProps[prop.name]"
:options="prop.options" :options="prop.options"
:name="prop.name" :name="`prop-${prop.name}`"
:label="componentProps[prop.name]" :label="componentProps[prop.name]"
appearance="none" variant="none"
class="inline-flex" class="inline-flex"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }" :ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
:ui-select="{ custom: '!py-0' }" :ui-select="{ custom: '!py-0' }"
@@ -26,8 +26,8 @@
v-else v-else
:model-value="componentProps[prop.name]" :model-value="componentProps[prop.name]"
:type="prop.type === 'number' ? 'number' : 'text'" :type="prop.type === 'number' ? 'number' : 'text'"
:name="prop.name" :name="`prop-${prop.name}`"
appearance="none" variant="none"
autocomplete="off" autocomplete="off"
:ui="{ custom: '!py-0' }" :ui="{ custom: '!py-0' }"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val" @update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
@@ -35,9 +35,15 @@
</div> </div>
</div> </div>
<div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass]"> <div class="flex border border-b-0 border-gray-200 dark:border-gray-700 relative not-prose" :class="[{ 'p-4': padding }, propsToSelect.length ? 'border-t-0' : 'rounded-t-md', backgroundClass, overflowClass]">
<component :is="name" v-model="vModel" v-bind="fullProps"> <component :is="name" v-model="vModel" v-bind="fullProps">
<ContentSlot v-if="$slots.default" :use="$slots.default" /> <ContentSlot v-if="$slots.default" :use="$slots.default" />
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<ClientOnly>
<ContentSlot v-if="$slots[slot]" :use="$slots[slot]" />
</ClientOnly>
</template>
</component> </component>
</div> </div>
@@ -49,6 +55,7 @@
// @ts-expect-error // @ts-expect-error
import { transformContent } from '@nuxt/content/transformers' import { transformContent } from '@nuxt/content/transformers'
// eslint-disable-next-line vue/no-dupe-keys
const props = defineProps({ const props = defineProps({
slug: { slug: {
type: String, type: String,
@@ -66,6 +73,10 @@ const props = defineProps({
type: String, type: String,
default: null default: null
}, },
slots: {
type: Object,
default: null
},
baseProps: { baseProps: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
@@ -78,17 +89,27 @@ const props = defineProps({
type: Array, type: Array,
default: () => [] default: () => []
}, },
extraColors: {
type: Array,
default: () => []
},
backgroundClass: { backgroundClass: {
type: String, type: String,
default: 'bg-white dark:bg-gray-900' default: 'bg-white dark:bg-gray-900'
},
overflowClass: {
type: String,
default: ''
} }
}) })
// eslint-disable-next-line vue/no-dupe-keys
const baseProps = reactive({ ...props.baseProps }) const baseProps = reactive({ ...props.baseProps })
const componentProps = reactive({ ...props.props }) const componentProps = reactive({ ...props.props })
const appConfig = useAppConfig() const appConfig = useAppConfig()
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`
@@ -97,6 +118,7 @@ const meta = await fetchComponentMeta(name)
// Computed // Computed
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui })) const ui = computed(() => ({ ...appConfig.ui[camelName], ...props.ui }))
const fullProps = computed(() => ({ ...props.baseProps, ...componentProps })) const fullProps = computed(() => ({ ...props.baseProps, ...componentProps }))
@@ -117,7 +139,8 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
const keys = useGet(ui.value, dottedKey, {}) const keys = useGet(ui.value, dottedKey, {})
let options = typeof keys === 'object' && Object.keys(keys) let options = typeof keys === 'object' && Object.keys(keys)
if (key.toLowerCase().endsWith('color')) { if (key.toLowerCase().endsWith('color')) {
options = appConfig.ui.colors // @ts-ignore
options = [...appConfig.ui.colors, ...props.extraColors]
} }
return { return {
@@ -128,6 +151,7 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
} }
}).filter(Boolean)) }).filter(Boolean))
// eslint-disable-next-line vue/no-dupe-keys
const code = computed(() => { const code = computed(() => {
let code = `\`\`\`html let code = `\`\`\`html
<${name}` <${name}`
@@ -140,7 +164,14 @@ const code = computed(() => {
code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}` code += ` ${(prop?.type === 'boolean' && value !== true) || typeof value === 'object' ? ':' : ''}${key === 'modelValue' ? 'value' : useKebabCase(key)}${prop?.type === 'boolean' && !!value && key !== 'modelValue' ? '' : `="${typeof value === 'object' ? renderObject(value) : value}"`}`
} }
if (props.code) {
if (props.slots) {
code += `>
${Object.entries(props.slots).map(([key, value]) => `<template #${key}>
${value}
</template>`).join('\n ')}
</${name}>`
} else if (props.code) {
const lineBreaks = (props.code.match(/\n/g) || []).length const lineBreaks = (props.code.match(/\n/g) || []).length
if (lineBreaks > 1) { if (lineBreaks > 1) {
code += `> code += `>
@@ -173,7 +204,7 @@ function renderObject (obj: any) {
return obj return obj
} }
const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(componentProps)}`, () => transformContent('content:_markdown.md', code.value, { const { data: ast } = await useAsyncData(`${name}-ast-${JSON.stringify(props)}`, () => transformContent('content:_markdown.md', code.value, {
highlight: { highlight: {
theme: { theme: {
light: 'material-lighter', light: 'material-lighter',

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="[&>div>pre]:!rounded-t-none"> <div class="[&>div>pre]:!rounded-t-none">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]"> <div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass, overflowClass]">
<ContentSlot v-if="$slots.default" :use="$slots.default" /> <ContentSlot v-if="$slots.default" :use="$slots.default" />
</div> </div>
@@ -17,6 +17,10 @@ defineProps({
backgroundClass: { backgroundClass: {
type: String, type: String,
default: 'bg-white dark:bg-gray-900' default: 'bg-white dark:bg-gray-900'
},
overflowClass: {
type: String,
default: ''
} }
}) })
</script> </script>

View File

@@ -15,6 +15,7 @@ const props = defineProps({
const appConfig = useAppConfig() const appConfig = useAppConfig()
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`

View File

@@ -43,6 +43,7 @@ const props = defineProps({
}) })
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`

View File

@@ -26,6 +26,7 @@ const props = defineProps({
}) })
const route = useRoute() const route = useRoute()
// eslint-disable-next-line vue/no-dupe-keys
const slug = props.slug || route.params.slug[1] const slug = props.slug || route.params.slug[1]
const camelName = useCamelCase(slug) const camelName = useCamelCase(slug)
const name = `U${useUpperFirst(camelName)}` const name = `U${useUpperFirst(camelName)}`

View File

@@ -11,6 +11,7 @@ const props = defineProps({
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
const colorMode = useColorMode()
const src = computed(() => `https://volta.net/embed/${props.token}?gray=${appConfig.ui.gray}&primary=${appConfig.ui.primary}`) const src = computed(() => `https://volta.net/embed/${props.token}?theme=${colorMode.value}&gray=${appConfig.ui.gray}&primary=${appConfig.ui.primary}`)
</script> </script>

View File

@@ -8,11 +8,15 @@ const items = [
}], [{ }], [{
label: 'Edit', label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid', icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E'] shortcuts: ['E'],
click: () => {
console.log('Edit')
}
}, { }, {
label: 'Duplicate', label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid', icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D'] shortcuts: ['D'],
disabled: true
}], [{ }], [{
label: 'Archive', label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid' icon: 'i-heroicons-archive-box-20-solid'

View File

@@ -0,0 +1,16 @@
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}]
]
</script>
<template>
<UDropdown :items="items" mode="hover" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>

View File

@@ -0,0 +1,18 @@
<template>
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
<template #trailing>
<UButton
v-show="q !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="q = ''"
/>
</template>
</UInput>
</template>
<script setup lang="ts">
const q = ref('')
</script>

View File

@@ -6,10 +6,10 @@ const selected = ref(people[3])
<template> <template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people"> <USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton> <UButton color="gray">
{{ selected }} {{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" /> <UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
</UButton> </UButton>
</USelectMenu> </USelectMenu>
</template> </template>

View File

@@ -5,10 +5,5 @@ const selected = ref([])
</script> </script>
<template> <template>
<USelectMenu v-model="selected" :options="people" multiple> <USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
<template #label>
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
</template>
</USelectMenu>
</template> </template>

View File

@@ -0,0 +1,14 @@
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
<span v-else>Select people</span>
</template>
</USelectMenu>
</template>

View File

@@ -7,8 +7,23 @@ const isOpen = ref(false)
<UButton label="Open" @click="isOpen = true" /> <UButton label="Open" @click="isOpen = true" />
<USlideover v-model="isOpen"> <USlideover v-model="isOpen">
<div class="p-4 h-full"> <div class="p-4 sm:p-6 flex flex-col flex-1 gap-4 sm:gap-6">
<Placeholder class="w-full h-full" /> <div class="flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">
Title
</h2>
<UButton
icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="link"
size="md"
:padded="false"
@click="isOpen = false"
/>
</div>
<Placeholder class="flex-1 w-full" />
</div> </div>
</USlideover> </USlideover>
</div> </div>

View File

@@ -0,0 +1,43 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :rows="people" />
</template>

View File

@@ -0,0 +1,59 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>

View File

@@ -0,0 +1,68 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const selectedColumns = ref([...columns])
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<div>
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
</div>
<UTable :columns="selectedColumns" :rows="people" />
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'email',
label: 'Email',
sortable: true,
direction: 'desc'
}, {
key: 'role',
label: 'Role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
</template>

View File

@@ -0,0 +1,80 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const q = ref('')
const filteredRows = computed(() => {
if (!q.value) {
return people
}
return people.filter((person) => {
return Object.values(person).some((value) => {
return String(value).toLowerCase().includes(q.value.toLowerCase())
})
})
})
</script>
<template>
<div>
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
<UInput v-model="q" placeholder="Filter people..." />
</div>
<UTable :rows="filteredRows" :columns="columns" />
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" />
</template>

View File

@@ -0,0 +1,91 @@
<script setup>
const columns = [{
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}, {
key: 'actions'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const items = (row) => [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: () => console.log('Edit', row.id)
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns">
<template #name-data="{ row }">
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
</UDropdown>
</template>
</UTable>
</template>

View File

@@ -3,7 +3,8 @@ const links = [{
label: 'Profile', label: 'Profile',
avatar: { avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
} },
badge: 100
}, { }, {
label: 'Installation', label: 'Installation',
icon: 'i-heroicons-home', icon: 'i-heroicons-home',

View File

@@ -17,8 +17,8 @@ const groups = computed(() => navigation.value.map(item => ({
})) }))
}))) })))
const close = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null) const closeButton = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-x-mark', color: 'black', variant: 'ghost', size: 'lg', padded: false }) : null)
const empty = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' })) const emptyState = computed(() => commandPaletteRef.value?.query ? ({ icon: 'i-heroicons-magnifying-glass', queryLabel: 'No results' }) : ({ icon: '', label: 'No recent searches' }))
const ui = { const ui = {
wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800', wrapper: 'flex flex-col flex-1 min-h-0 bg-gray-50 dark:bg-gray-800',
@@ -50,7 +50,7 @@ const ui = {
} }
} }
}, },
empty: { emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 py-9', wrapper: 'flex flex-col items-center justify-center flex-1 py-9',
label: 'text-sm text-center text-gray-500 dark:text-gray-400', label: 'text-sm text-center text-gray-500 dark:text-gray-400',
queryLabel: 'text-lg text-center text-gray-900 dark:text-white', queryLabel: 'text-lg text-center text-gray-900 dark:text-white',
@@ -64,8 +64,8 @@ const ui = {
ref="commandPaletteRef" ref="commandPaletteRef"
:groups="groups" :groups="groups"
:ui="ui" :ui="ui"
:close="close" :close-button="closeButton"
:empty="empty" :empty-state="emptyState"
:autoselect="false" :autoselect="false"
command-attribute="title" command-attribute="title"
:fuse="{ :fuse="{

View File

@@ -0,0 +1,28 @@
<script setup>
const links = [{
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>

View File

@@ -10,8 +10,11 @@
class="mt-1" class="mt-1"
:ui="{ :ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2', wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4', padding: 'pl-4',
base: 'group text-sm block border-l -ml-px lg:leading-6', rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold', active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300' inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}" }"

View File

@@ -3,7 +3,7 @@
<div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400"> <div class="flex items-baseline gap-1.5 text-sm text-center text-gray-500 dark:text-gray-400">
Made by Made by
<NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs"> <NuxtLink to="https://nuxtlabs.com" aria-label="NuxtLabs">
<LogoLabs class="text-white w-14 h-auto" /> <LogoLabs class="text-primary-500 w-14 h-auto dark:text-primary-400" />
</NuxtLink> </NuxtLink>
</div> </div>
</footer> </footer>

View File

@@ -1,7 +1,7 @@
<template> <template>
<header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12"> <header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12">
<p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize"> <p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize">
{{ useLowerCase(page._dir) }} {{ page._dir?.title ? page._dir.title : useLowerCase(page._dir) }}
</p> </p>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between"> <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white"> <h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white">
@@ -22,7 +22,7 @@
label="GitHub" label="GitHub"
icon="i-simple-icons-github" icon="i-simple-icons-github"
color="white" color="white"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}.vue`" :to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/${page.title.replace(' ', '')}${page.github.suffix || '.vue'}`"
/> />
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="grid gap-6 sm:grid-cols-2"> <div class="grid gap-6 sm:grid-cols-2">
<DocsPrevNextCard v-if="prev" :title="prev.navigation?.title || prev.title" :description="prev.navigation?.description || prev.description" :to="prev._path" icon="i-heroicons-arrow-left-20-solid" /> <DocsPrevNextCard v-if="prev" :title="prev.navigation?.title || prev.title" :description="prev.navigation?.description || prev.description" :to="prev._path" icon="i-heroicons-arrow-left-20-solid" />
<span v-else>&nbsp;</span> <span v-else class="hidden sm:block">&nbsp;</span>
<DocsPrevNextCard <DocsPrevNextCard
v-if="next" v-if="next"
:title="next.navigation?.title || next.title" :title="next.navigation?.title || next.title"

View File

@@ -10,7 +10,16 @@ export async function fetchComponentMeta (name: string) {
if (state.value[name]) { return state.value[name] } if (state.value[name]) { return state.value[name] }
// Store promise to avoid multiple calls // Store promise to avoid multiple calls
state.value[name] = $fetch(`/api/component-meta/${name}`).then((meta) => {
// add to nitro prerender
if (process.server) {
const event = useRequestEvent()
event.node.res.setHeader(
'x-nitro-prerender',
[event.node.res.getHeader('x-nitro-prerender'), `/api/component-meta/${name}.json`].filter(Boolean).join(',')
)
}
state.value[name] = $fetch(`/api/component-meta/${name}.json`).then((meta) => {
state.value[name] = meta state.value[name] = meta
}) })

View File

@@ -19,15 +19,21 @@ export default defineAppConfig({
}) })
``` ```
::alert{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors in the navbar and see the documentation change live.
::
As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`. As this module uses Tailwind CSS under the hood, you can use any of the [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) or your own custom colors. By default, the `primary` color is `green` and the `gray` color is `cool`.
To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released). To provide dynamic colors that can be changed at runtime, this module uses CSS variables. As Tailwind CSS already has a `gray` color, the module automatically renames it to `cool` to avoid conflicts (`coolGray` was renamed to `gray` when Tailwind CSS v3.0 was released).
Likewise, you can't define a `primary` color in your `tailwind.config.ts` as it would conflict with the `primary` color defined by the module.
::alert{icon="i-heroicons-light-bulb"} ::alert{icon="i-heroicons-light-bulb"}
Try to change the `primary` and `gray` colors in the navbar and see the colors change live. We'd advise you to use those colors in your components and pages, e.g. `text-primary-500 dark:text-primary-400`, `bg-gray-100 dark:bg-gray-900`, etc. so your app automatically adapts when changing your `app.config.ts`.
:: ::
Components that have a `color` prop like [Avatar](/elements/avatar), [Badge](/elements/badge) and [Button](/elements/button) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors. Components that have a `color` prop like [Avatar](/elements/avatar#chip), [Badge](/elements/badge#style), [Button](/elements/button#style) and [Notification](/overlays/notification#timeout) will use the `primary` color by default but will handle all the colors defined in your `tailwind.config.ts` or the default Tailwind CSS colors.
## Dark mode ## Dark mode
@@ -160,6 +166,7 @@ export default defineAppConfig({
}, },
select: { select: {
default: { default: {
loadingIcon: 'i-octicon-sync-24',
trailingIcon: 'i-octicon-chevron-down-24' trailingIcon: 'i-octicon-chevron-down-24'
} }
}, },
@@ -170,7 +177,7 @@ export default defineAppConfig({
}, },
notification: { notification: {
default: { default: {
close: { closeButton: {
icon: 'i-octicon-x-24' icon: 'i-octicon-x-24'
} }
} }
@@ -178,11 +185,24 @@ export default defineAppConfig({
commandPalette: { commandPalette: {
default: { default: {
icon: 'i-octicon-search-24', icon: 'i-octicon-search-24',
loadingIcon: 'i-octicon-sync-24',
selectedIcon: 'i-octicon-check-24', selectedIcon: 'i-octicon-check-24',
empty: { emptyState: {
icon: 'i-octicon-search-24' icon: 'i-octicon-search-24'
} }
} }
},
table: {
default: {
sortAscIcon: 'i-octicon-sort-asc-24',
sortDescIcon: 'i-octicon-sort-desc-24',
sortButton: {
icon: 'i-octicon-arrow-switch-24'
},
emptyState: {
icon: 'i-octicon-database-24'
}
}
} }
} }
}) })

View File

@@ -0,0 +1 @@
title: Getting Started

View File

@@ -29,14 +29,15 @@ baseProps:
### Chip ### Chip
Use the `chipColor`, `chipVariant` and `chipPosition` props to display a chip on the Avatar. Use the `chip-color` and `chip-position` props to display a chip on the Avatar.
::component-card ::component-card
--- ---
props: props:
chipColor: 'primary' chipColor: 'primary'
chipVariant: 'solid'
chipPosition: 'top-right' chipPosition: 'top-right'
extraColors:
- gray
baseProps: baseProps:
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar' alt: 'Avatar'
@@ -53,6 +54,7 @@ If there's an `alt` prop initials will be displayed on top of the background, cu
--- ---
props: props:
alt: 'Benjamin Canac' alt: 'Benjamin Canac'
size: 'sm'
--- ---
:: ::

View File

@@ -113,7 +113,7 @@ Button
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position. Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card ::component-card
--- ---
@@ -163,7 +163,7 @@ Button
Use the `loading` prop to show a loading icon and disable the Button. Use the `loading` prop to show a loading icon and disable the Button.
Use the `loadingIcon` prop to set a different icon or change it globally in `ui.button.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`. Use the `loading-icon` prop to set a different icon or change it globally in `ui.button.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card ::component-card
--- ---
@@ -275,6 +275,48 @@ code: |
:u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"} :u-button{icon="i-heroicons-chevron-down-20-solid" color="gray"}
:: ::
## Slots
### `leading`
Use the `#leading` slot to set the content of the leading icon.
::component-card
---
slots:
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
baseProps:
color: 'gray'
props:
label: Button
color: 'gray'
excludedProps:
- color
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
::
### `trailing`
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <UIcon name="i-heroicons-arrow-right-20-solid" />
props:
label: Button
color: 'gray'
excludedProps:
- color
---
#trailing
:u-icon{name="i-heroicons-arrow-right-20-solid"}
::
## Props ## Props
:component-props :component-props

View File

@@ -8,9 +8,20 @@ headlessui:
## Usage ## Usage
Pass an array of arrays to the `items` prop of the Dropdown component. Each array represents a group of items. Each item can have the following properties:
- `label` - The label of the item.
- `icon` - The icon of the item.
- `avatar` - The avatar of the item. You can pass all the props of the [Avatar](/elements/avatar) component.
- `shortcuts` - The shortcuts of the item.
- `disabled` - Whether the item is disabled.
- `click` - The click handler of the item.
You can also pass properties from the [NuxtLink](https://nuxt.com/docs/api/components/nuxt-link#props) component such as `to`, `exact`, etc.
::component-example ::component-example
#default #default
:dropdown-example :dropdown-example-basic
#code #code
```vue ```vue
@@ -24,11 +35,15 @@ const items = [
}], [{ }], [{
label: 'Edit', label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid', icon: 'i-heroicons-pencil-square-20-solid',
shortcuts: ['E'] shortcuts: ['E'],
click: () => {
console.log('Edit')
}
}, { }, {
label: 'Duplicate', label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid', icon: 'i-heroicons-document-duplicate-20-solid',
shortcuts: ['D'] shortcuts: ['D'],
disabled: true
}], [{ }], [{
label: 'Archive', label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid' icon: 'i-heroicons-archive-box-20-solid'
@@ -51,6 +66,35 @@ const items = [
``` ```
:: ::
### Mode
Use the `mode` prop to switch between `click` and `hover` modes.
::component-example
#default
:dropdown-example-mode
#code
```vue
<script setup>
const items = [
[{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}]
]
</script>
<template>
<UDropdown :items="items" mode="hover" :popper="{ placement: 'bottom-start' }">
<UButton color="white" label="Options" trailing-icon="i-heroicons-chevron-down-20-solid" />
</UDropdown>
</template>
```
::
## Props ## Props
:component-props :component-props

View File

@@ -12,6 +12,53 @@ baseProps:
--- ---
:: ::
### Style
Use the `color` and `variant` props to change the visual style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size ### Size
Use the `size` prop to change the size of the Input. Use the `size` prop to change the size of the Input.
@@ -38,25 +85,11 @@ props:
--- ---
:: ::
### Appearance
Use the `appearance` prop to change the style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `leading` and `trailing` props to set the icon position or the `leadingIcon` and `trailingIcon` props to set a different icon for each position. Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::component-card ::component-card
--- ---
@@ -65,9 +98,12 @@ baseProps:
placeholder: 'Search...' placeholder: 'Search...'
props: props:
icon: 'i-heroicons-magnifying-glass-20-solid' icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
size: 'sm' size: 'sm'
color: 'white'
trailing: false trailing: false
extraColors:
- white
- gray
excludedProps: excludedProps:
- icon - icon
--- ---
@@ -81,12 +117,9 @@ Use the `disabled` prop to disable the Input.
--- ---
baseProps: baseProps:
name: 'input' name: 'input'
props:
placeholder: 'Search...' placeholder: 'Search...'
appearance: 'white' props:
disabled: true disabled: true
excludedProps:
- placeholder
--- ---
:: ::
@@ -94,7 +127,7 @@ excludedProps:
Use the `loading` prop to show a loading icon and disable the Input. Use the `loading` prop to show a loading icon and disable the Input.
Use the `loadingIcon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`. Use the `loading-icon` prop to set a different icon or change it globally in `ui.input.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card ::component-card
--- ---
@@ -109,30 +142,73 @@ excludedProps:
--- ---
:: ::
### Group ## Slots
You can use the `InputGroup` component to add a label and additional informations to a form element. ### `leading`
::component-card{slug="InputGroup"} Use the `#leading` slot to set the content of the leading icon.
::component-card
--- ---
slots:
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
baseProps: baseProps:
name: 'group' name: 'input'
props: placeholder: 'Search...'
label: 'Email'
help: "We'll only use this for spam."
hint: 'Required'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
--- ---
#default #leading
:u-input{name="group" placeholder="you@example.com" icon="i-heroicons-envelope"} :u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
:: ::
::alert{icon="i-heroicons-light-bulb"} ### `trailing`
This also works with `Textarea`, `Select` and `SelectMenu` components.
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
baseProps:
name: 'input'
placeholder: 'Search...'
---
#trailing
[EUR]{class="text-gray-500 dark:text-gray-400 text-xs"}
::
You can for example create a clearable Input by injecting a [Button](/elements/button) in the `trailing` slot that displays when some text is entered.
::component-example
#default
:input-example-clearable
#code
```vue
<template>
<UInput v-model="q" name="q" placeholder="Search..." icon="i-heroicons-magnifying-glass-20-solid" :ui="{ icon: { trailing: { pointer: '' } } }">
<template #trailing>
<UButton
v-show="q !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="q = ''"
/>
</template>
</UInput>
</template>
<script setup lang="ts">
const q = ref('')
</script>
```
::
::alert{icon="i-heroicons-exclamation-triangle-20-solid"}
As leading and trailing icons are wrapped around a `pointer-events-none` class, if you inject a clickable element in the slot, you need to remove this class to make it clickable by adding `:ui="{ icon: { trailing: { pointer: '' } } }"` to the Input.
:: ::
## Props ## Props

View File

@@ -12,6 +12,53 @@ baseProps:
--- ---
:: ::
### Style
Use the `color` and `variant` props to change the visual style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size ### Size
Use the `size` prop to change the size of the Textarea. Use the `size` prop to change the size of the Textarea.
@@ -38,17 +85,17 @@ props:
--- ---
:: ::
### Appearance ### Rows
Use the `appearance` prop to change the style of the Textarea. Use the `rows` prop to set the number of rows of the Textarea.
::component-card ::component-card
--- ---
baseProps: baseProps:
name: 'textarea' name: 'input'
placeholder: 'Search...' placeholder: 'Search...'
props: props:
appearance: 'white' rows: 1
--- ---
:: ::
@@ -62,11 +109,39 @@ baseProps:
name: 'input' name: 'input'
placeholder: 'Search...' placeholder: 'Search...'
props: props:
appearance: 'white'
disabled: true disabled: true
--- ---
:: ::
### Autoresize
Use the `autoresize` prop to enable the autoresize. Writing more lines than the `rows` prop will make the Textarea grow up.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
modelValue: 'Here is an autoresize Textarea, write new lines to make the Textarea grow up...'
props:
autoresize: true
---
::
### Resize
Use the `resize` prop to enable the resize control.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
resize: true
---
::
## Props ## Props
:component-props :component-props

View File

@@ -22,9 +22,65 @@ excludedProps:
--- ---
:: ::
### Style
Use the `color` and `variant` props to change the visual style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size ### Size
Use the `size` prop to change the size of the Input. Use the `size` prop to change the size of the Select.
::component-card ::component-card
--- ---
@@ -56,29 +112,11 @@ props:
--- ---
:: ::
### Appearance
Use the `appearance` prop to change the style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `trailingIcon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`. Use the `trailing-icon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
::component-card ::component-card
--- ---
@@ -91,8 +129,11 @@ baseProps:
placeholder: 'Search...' placeholder: 'Search...'
props: props:
icon: 'i-heroicons-magnifying-glass-20-solid' icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white' color: 'white'
size: 'sm' size: 'sm'
extraColors:
- white
- gray
excludedProps: excludedProps:
- icon - icon
--- ---
@@ -100,7 +141,7 @@ excludedProps:
### Disabled ### Disabled
Use the `disabled` prop to disable the Input. Use the `disabled` prop to disable the Select.
::component-card ::component-card
--- ---
@@ -112,11 +153,73 @@ baseProps:
- 'Mexico' - 'Mexico'
placeholder: 'Search...' placeholder: 'Search...'
props: props:
appearance: 'white'
disabled: true disabled: true
--- ---
:: ::
### Loading
Use the `loading` prop to show a loading icon and disable the Input.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.select.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
props:
loading: true
icon: 'i-heroicons-magnifying-glass-20-solid'
excludedProps:
- icon
---
::
## Slots
### `leading`
Use the `#leading` slot to set the content of the leading icon.
::component-card
---
slots:
leading: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
---
#leading
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs"}
::
### `trailing`
Use the `#trailing` slot to set the content of the trailing icon.
::component-card
---
slots:
trailing: <UIcon name="i-heroicons-arrows-up-down-20-solid" />
baseProps:
name: 'input'
placeholder: 'Search...'
---
#trailing
:u-icon{name="i-heroicons-arrows-up-down-20-solid"}
::
## Props ## Props
:component-props :component-props

View File

@@ -8,7 +8,9 @@ headlessui:
## Usage ## Usage
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [size](/forms/select#size), [placeholder](/forms/select#placeholder), [appearance](/forms/select#appearance), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc. The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
### Options
Like the Select component, you can use the `options` prop to pass an array of strings or objects. Like the Select component, you can use the `options` prop to pass an array of strings or objects.
@@ -30,7 +32,9 @@ const selected = ref(people[0])
``` ```
:: ::
You can use the `multiple` prop to select multiple values but you have to override the `#label` slot and handle the display yourself. ### Multiple
You can use the `multiple` prop to select multiple values.
::component-example ::component-example
#default #default
@@ -39,49 +43,20 @@ You can use the `multiple` prop to select multiple values but you have to overri
#code #code
```vue ```vue
<script setup> <script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer'] const people = [...]
const selected = ref([]) const selected = ref([])
</script> </script>
<template> <template>
<USelectMenu v-model="selected" :options="people" multiple> <USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
<template #label>
<span v-if="selected.length" class="font-medium truncate">{{ selected.join(', ') }}</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">Select people</span>
</template>
</USelectMenu>
</template> </template>
``` ```
:: ::
You can also override the default slot entirely. ### Objects
::component-example You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `option-attribute` prop that defaults to `label`.
#default
:select-menu-example-button{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton>
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>
```
::
You can pass an array of objects to `options` and either compare on the whole object or use the `by` prop to compare on a specific key. You can configure which field will be used to display the label through the `optionAttribute` prop that defaults to `label`.
::component-example ::component-example
#default #default
@@ -134,11 +109,7 @@ const selected = ref(people[0])
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use the `selected-icon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
Use the `trailingIcon` prop to set a different icon or change it globally in `ui.select.default.trailingIcon`. Defaults to `i-heroicons-chevron-down-20-solid`.
Use the `selectedIcon` prop to set a different icon or change it globally in `ui.selectMenu.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card ::component-card
--- ---
@@ -147,16 +118,22 @@ baseProps:
placeholder: 'Select a person' placeholder: 'Select a person'
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer'] options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props: props:
icon: 'i-heroicons-magnifying-glass-20-solid' selectedIcon: 'i-heroicons-hand-thumb-up-solid'
excludedProps: excludedProps:
- icon - selectedIcon
--- ---
:: ::
::alert{icon="i-heroicons-light-bulb"}
Learn how to customize icons from the [Select](/forms/select#icon) component.
::
### Search ### Search
Use the `searchable` prop to enable search. Use the `searchable` prop to enable search.
Use the `searchable-placeholder` prop to set a different placeholder.
This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox). This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
::component-card ::component-card
@@ -167,9 +144,67 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer'] options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props: props:
searchable: true searchable: true
searchablePlaceholder: 'Search a person...'
--- ---
:: ::
## Slots
### `label`
You can override the `#label` slot and handle the display yourself.
::component-example
#default
:select-menu-example-multiple-slot{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = [...]
const selected = ref([])
</script>
<template>
<USelectMenu v-model="selected" :options="people" multiple>
<template #label>
<span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
<span v-else>Select people</span>
</template>
</USelectMenu>
</template>
```
::
### `default`
You can also override the `#default` slot entirely.
::component-example
#default
:select-menu-example-button{class="max-w-[12rem] w-full"}
#code
```vue
<script setup>
const people = [...]
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
</UButton>
</USelectMenu>
</template>
```
::
## Props ## Props
:component-props :component-props

View File

@@ -13,16 +13,16 @@ headlessui:
### Icon ### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon-on` and `icon-off` props by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `on-icon` and `off-icon` props by using this pattern: `i-{collection_name}-{icon_name}` or change it globally in `ui.toggle.default.onIcon` and `ui.toggle.default.offIcon`.
::component-card ::component-card
--- ---
props: props:
iconOn: 'i-heroicons-check-20-solid' onIcon: 'i-heroicons-check-20-solid'
iconOff: 'i-heroicons-x-mark-20-solid' offIcon: 'i-heroicons-x-mark-20-solid'
excludedProps: excludedProps:
- iconOn - onIcon
- iconOff - offIcon
--- ---
:: ::

View File

@@ -0,0 +1,141 @@
---
github:
suffix: .ts
description: Display a label and additional informations around a form element.
---
## Usage
Use the FormGroup component around an [Input](/forms/input), [Textarea](/forms/textarea), [Select](/forms/select) or a [SelectMenu](/forms/select-menu) with the `name` prop to automatically associate a `<label>` element with the form element.
::component-card
---
props:
name: 'email'
label: 'Email'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Required
Use the `required` prop to indicate that the form element is required.
::component-card
---
baseProps:
name: 'group-required'
props:
label: 'Email'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Description
Use the `description` prop to display a description below the label.
::component-card
---
baseProps:
name: 'group-description'
props:
label: 'Email'
description: "We'll only use this for spam."
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Hint
Use the `hint` prop to display a hint above the form element.
::component-card
---
baseProps:
name: 'group-hint'
props:
label: 'Email'
hint: 'Optional'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Help
Use the `help` prop to display an help message below the form element.
::component-card
---
baseProps:
name: 'group-help'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Error
Use the `error` prop to display an error message below the form element.
When used together with the `help` prop, the `error` prop will take precedence.
::component-card
---
baseProps:
name: 'group-error'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
error: "Not a valid email address."
code: >-
<UInput placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid" />
---
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid"}
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::alert{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,491 @@
---
github: true
description: 'Display data in a table.'
---
## Usage
Use the `rows` prop to set the data to display in the table. By default, the table will display all the fields of the rows.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-basic{class="flex-1"}
#code
```vue
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :rows="people" />
</template>
```
::
### Columns
Use the `columns` prop to configure which columns to display. It's an array of objects with the following properties:
- `label` - The label to display in the table header. Can be changed through the `column-attribute` prop.
- `key` - The field to display from the row data.
- `sortable` - Whether the column is sortable. Defaults to `false`.
- `direction` - The sort direction to use on first click. Defaults to `asc`.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-columns{class="flex-1"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}]
const people = [...]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>
```
::
You can easily use the [SelectMenu](/forms/select-menu) component to change the columns to display.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-columns-selectable{class="flex-1"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const selectedColumns = ref([...columns])
const people = [...]
</script>
<template>
<div>
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
<UTable :columns="selectedColumns" :rows="people" />
</div>
</template>
```
::
### Sortable
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-columns-sortable{class="flex-1"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'email',
label: 'Email',
sortable: true,
direction: 'desc'
}, {
key: 'role',
label: 'Role'
}]
const people = [...]
</script>
<template>
<UTable :columns="columns" :rows="people" :sort="{ column: 'title' }" />
</template>
```
::
You can specify the default direction of each column through the `direction` property. It can be either `asc` or `desc` and defaults to `asc`.
You can specify a default sort for the table through the `sort` prop. It's an object with the following properties:
- `column` - The column to sort by.
- `direction` - The sort direction. Can be either `asc` or `desc` and defaults to `asc`.
::alert{icon="i-heroicons-light-bulb"}
This will set the default sort and will work even if no column is set as `sortable`.
::
Use the `sort-button` prop to customize the sort button in the header. You can pass all the props of the [Button](/elements/button) component to customize it through this prop or globally through `ui.table.default.sortButton`. Its icon defaults to `i-heroicons-arrows-up-down-20-solid`.
::component-card{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
baseProps:
class: 'w-full'
columns:
- key: 'id'
label: 'ID'
- key: 'name'
label: 'Name'
sortable: true
- key: 'title'
label: 'Title'
sortable: true
- key: 'email'
label: 'Email'
sortable: true
- key: 'role'
label: 'Role'
rows:
- id: 1
name: 'Lindsay Walton'
title: 'Front-end Developer'
email: 'lindsay.walton@example.com'
role: 'Member'
- id: 2
name: 'Courtney Henry'
title: 'Designer'
email: 'courtney.henry@example.com'
role: 'Admin'
- id: 3
name: 'Tom Cook'
title: 'Director of Product'
email: 'tom.cook@example.com'
role: 'Member'
- id: 4
name: 'Whitney Francis'
title: 'Copywriter'
email: 'whitney.francis@example.com'
role: 'Admin'
- id: 5
name: 'Leonard Krasner'
title: 'Senior Designer'
email: 'leonard.krasner@example.com'
role: 'Owner'
- id: 6
name: 'Floyd Miles'
title: 'Principal Designer'
email: 'floyd.miles@example.com'
role: 'Member'
props:
sortAscIcon: 'i-heroicons-arrow-up-20-solid'
sortDescIcon: 'i-heroicons-arrow-down-20-solid'
sortButton:
icon: 'i-heroicons-sparkles-20-solid'
color: 'primary'
variant: 'outline'
size: '2xs'
square: false
ui:
rounded: 'rounded-full'
excludedProps:
- sortButton
- sortAscIcon
- sortDescIcon
---
::
Use the `sort-asc-icon` prop to set a different icon or change it globally in `ui.table.default.sortAscIcon`. Defaults to `i-heroicons-bars-arrow-up-20-solid`.
Use the `sort-desc-icon` prop to set a different icon or change it globally in `ui.table.default.sortDescIcon`. Defaults to `i-heroicons-bars-arrow-down-20-solid`.
::alert{icon="i-heroicons-light-bulb"}
You can also customize the entire header cell, read more in the [Slots](#slots) section.
::
### Selectable
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-selectable{class="flex-1"}
#code
```vue
<script setup>
const people = [...]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" />
</template>
```
::
::alert{icon="i-heroicons-light-bulb"}
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
::
### Searchable
You can easily use the [Input](/forms/input) component to filter the rows.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-searchable{class="flex-1"}
#code
```vue
<script setup>
const people = [...]
const q = ref('')
const filteredRows = computed(() => {
if (!q.value) {
return people
}
return people.filter((person) => {
return Object.values(person).some((value) => {
return String(value).toLowerCase().includes(q.value.toLowerCase())
})
})
})
</script>
<template>
<div>
<UInput v-model="q" placeholder="Filter people..." />
<UTable :rows="filteredRows" />
</div>
</template>
```
::
### Empty
Use the `empty-state` prop to display a message when there are no results.
You can pass an `object` through the `empty-state` prop or globally through `ui.table.default.emptyState`.
You can also set it to `null` to hide the empty state.
::component-card
---
padding: false
overflowClass: 'overflow-x-auto'
baseProps:
class: 'w-full'
columns:
- key: 'id'
label: 'ID'
- key: 'name'
label: 'Name'
- key: 'title'
label: 'Title'
- key: 'email'
label: 'Email'
- key: 'role'
label: 'Role'
props:
emptyState:
icon: 'i-heroicons-circle-stack-20-solid'
label: "No items."
excludedProps:
- emptyState
---
::
## Slots
You can use slots to customize the header and data cells of the table.
### `<column>-header`
Use the `#<column>-header` slot to customize the header cell of a column. You will have access to the `column`, `sort` and `on-sort` properties in the slot scope.
The `sort` property is an object with the following properties:
- `field` - The field to sort by.
- `direction` - The direction to sort by. Can be `asc` or `desc`.
The `on-sort` property is a function that you can call to sort the table and accepts the column as parameter.
::alert{icon="i-heroicons-light-bulb"}
Even though you can customize the sort button as mentioned in the [Sortable](#sortable) section, you can use this slot to completely override its behavior, with a custom dropdown for example.
::
### `<column>-data`
Use the `#<column>-data` slot to customize the data cell of a column. You will have access to the `row` and `column` properties in the slot scope.
You can for example create an extra column for actions with a [Dropdown](/elements/dropdown) component inside or change the color of the rows based on a selection.
::component-example{class="grid"}
---
padding: false
overflowClass: 'overflow-x-auto'
---
#default
:table-example-slots{class="flex-1"}
#code
```vue
<script setup>
const columns = [..., {
key: 'actions'
}]
const people = [...]
const items = (row) => [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: () => console.log('Edit', row.id)
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns">
<template #name-data="{ row }">
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
</UDropdown>
</template>
</UTable>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -1,47 +0,0 @@
---
github: true
description: Display a vertical navigation.
---
## Usage
::component-example
#default
:vertical-navigation-example
#code
```vue
<script setup>
const links = [{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
}, {
label: 'Installation',
icon: 'i-heroicons-home',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
icon: 'i-heroicons-chart-bar',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
icon: 'i-heroicons-command-line',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation :links="links" />
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -0,0 +1,90 @@
---
github: true
description: Display a vertical navigation.
---
## Usage
::component-example
#default
:vertical-navigation-example
#code
```vue
<script setup>
const links = [{
label: 'Profile',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
},
badge: 100
}, {
label: 'Installation',
icon: 'i-heroicons-home',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
icon: 'i-heroicons-chart-bar',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
icon: 'i-heroicons-command-line',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation :links="links" />
</template>
```
::
## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.
### Tailwind
::component-example
#default
:vertical-navigation-theme-tailwind
#code
```vue
<script setup>
const links = [{
label: 'Installation',
to: '/getting-started/installation'
}, {
label: 'Vertical Navigation',
to: '/navigation/vertical-navigation'
}, {
label: 'Command Palette',
to: '/navigation/command-palette'
}]
</script>
<template>
<UVerticalNavigation
:links="links"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-l -ml-px lg:leading-6',
padding: 'pl-4',
rounded: '',
font: '',
ring: '',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -160,13 +160,13 @@ function onSelect (option) {
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`. Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
Use the `selectedIcon` prop to set a different icon or change it globally in `ui.commandPalette.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`. Use the `selected-icon` prop to set a different icon or change it globally in `ui.commandPalette.default.selectedIcon`. Defaults to `i-heroicons-check-20-solid`.
::component-card ::component-card
--- ---
padding: false padding: false
baseProps: baseProps:
empty: null emptyState: null
props: props:
icon: 'i-heroicons-command-line' icon: 'i-heroicons-command-line'
excludedProps: excludedProps:
@@ -174,6 +174,24 @@ excludedProps:
--- ---
:: ::
### Loading
Use the `loading` prop to show a loading icon.
Use the `loading-icon` prop to set a different icon or change it globally in `ui.commandPalette.default.loadingIcon`. Defaults to `i-heroicons-arrow-path-20-solid`.
::component-card
---
padding: false
baseProps:
emptyState: null
props:
loading: true
excludedProps:
- icon
---
::
### Placeholder ### Placeholder
Use the `placeholder` prop to change the input placeholder Use the `placeholder` prop to change the input placeholder
@@ -182,7 +200,7 @@ Use the `placeholder` prop to change the input placeholder
--- ---
padding: false padding: false
baseProps: baseProps:
empty: null emptyState: null
props: props:
placeholder: 'Type a command...' placeholder: 'Type a command...'
excludedProps: excludedProps:
@@ -192,31 +210,33 @@ excludedProps:
### Close ### Close
Use the `close` prop to display a close button on the right side of the input. Use the `close-button` prop to display a close button on the right side of the input.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.commandPalette.default.close`. You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.commandPalette.default.closeButton`.
::component-card ::component-card
--- ---
padding: false padding: false
baseProps: baseProps:
empty: null emptyState: null
props: props:
close: closeButton:
icon: 'i-heroicons-x-mark-20-solid' icon: 'i-heroicons-x-mark-20-solid'
color: 'gray' color: 'gray'
variant: 'link' variant: 'link'
padded: false padded: false
excludedProps: excludedProps:
- close - closeButton
--- ---
:: ::
### Empty ### Empty
Use the `empty` prop to display a message when there are no results. Use the `empty-state` prop to display a message when there are no results.
You can pass an `object` through the `empty` prop or globally through `ui.commandPalette.default.empty`. Here is the default: You can pass an `object` through the `empty-state` prop or globally through `ui.commandPalette.default.emptyState`.
You can also set it to `null` to hide the empty state.
::component-card ::component-card
--- ---
@@ -224,12 +244,12 @@ padding: false
baseProps: baseProps:
placeholder: 'Type something to see the empty label change' placeholder: 'Type something to see the empty label change'
props: props:
empty: emptyState:
icon: 'i-heroicons-magnifying-glass-20-solid' icon: 'i-heroicons-magnifying-glass-20-solid'
label: "We couldn't find any items." label: "We couldn't find any items."
queryLabel: "We couldn't find any items with that term. Please try again." queryLabel: "We couldn't find any items with that term. Please try again."
excludedProps: excludedProps:
- empty - emptyState
--- ---
:: ::
@@ -237,7 +257,7 @@ excludedProps:
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop. The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `commandAttribute` prop. This will also affect the display of the command. When searching for a command, the component will look for a `label` property on the command by default. You can customize this behavior by overriding the `command-attribute` prop. This will also affect the display of the command.
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you. You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
@@ -299,6 +319,10 @@ const groups = computed(() => {
``` ```
:: ::
::alert{icon="i-heroicons-light-bulb"}
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
::
## Themes ## Themes
Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do. Our theming system provides a lot of flexibility to customize the component. Here is some examples of what you can do.

View File

@@ -36,7 +36,7 @@ const toast = useToast()
``` ```
:: ::
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`: This component will render by default the notifications at the bottom right of the screen. You can configure its behavior in the `app.config.ts` through `ui.notifications`:
```ts [app.config.ts] ```ts [app.config.ts]
export default defineAppConfig({ export default defineAppConfig({
@@ -92,8 +92,8 @@ baseProps:
id: 4 id: 4
timeout: 0 timeout: 0
title: 'Notification' title: 'Notification'
description: 'This is a notification.'
props: props:
description: 'This is a notification.'
avatar: avatar:
src: 'https://avatars.githubusercontent.com/u/739984?v=4' src: 'https://avatars.githubusercontent.com/u/739984?v=4'
excludedProps: excludedProps:
@@ -114,7 +114,28 @@ baseProps:
title: 'Notification' title: 'Notification'
description: 'This is a notification.' description: 'This is a notification.'
props: props:
timeout: 10000 timeout: 60000
---
::
### Color
Use the `color` prop to change the progress and icon color of the Notification.
::component-card
---
baseProps:
id: 5
title: 'Notification'
description: 'This is a notification.'
timeout: 600000
props:
icon: 'i-heroicons-x-circle'
color: 'red'
extraColors:
- gray
excludedProps:
- icon
--- ---
:: ::
@@ -166,9 +187,9 @@ function onCallback () {
### Close ### Close
Use the `close` prop to hide or customize the close button on the Notification. Use the `close-button` prop to hide or customize the close button on the Notification.
You can pass all the props of the [Button](/elements/button) component to customize it through the `close` prop or globally through `ui.notifications.default.close`. You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.notification.default.closeButton`.
::component-card ::component-card
--- ---
@@ -177,7 +198,7 @@ baseProps:
title: 'Notification' title: 'Notification'
timeout: 0 timeout: 0
props: props:
close: closeButton:
icon: 'i-heroicons-archive-box-x-mark' icon: 'i-heroicons-archive-box-x-mark'
color: 'primary' color: 'primary'
variant: 'outline' variant: 'outline'
@@ -186,7 +207,7 @@ props:
ui: ui:
rounded: 'rounded-full' rounded: 'rounded-full'
excludedProps: excludedProps:
- close - closeButton
--- ---
:: ::
@@ -209,7 +230,7 @@ const toast = useToast()
``` ```
:: ::
Like for `close`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notifications.default.action`. Like for `closeButton`, you can pass all the props of the [Button](/elements/button) component inside the action or globally through `ui.notification.default.actionButton`.
::component-card ::component-card
--- ---

View File

@@ -27,10 +27,6 @@ description: Display a card for content with a header, body and footer.
:component-props :component-props
## Slots
:component-slots
## Preset ## Preset
:component-preset :component-preset

View File

@@ -17,6 +17,7 @@ export default defineNuxtConfig({
highlight: { highlight: {
theme: { theme: {
light: 'material-lighter', light: 'material-lighter',
default: 'material-default',
dark: 'material-palenight' dark: 'material-palenight'
}, },
preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini'] preload: ['json', 'js', 'ts', 'html', 'css', 'vue', 'diff', 'shell', 'markdown', 'yaml', 'bash', 'ini']
@@ -30,14 +31,18 @@ export default defineNuxtConfig({
strict: false, strict: false,
includeWorkspace: true includeWorkspace: true
}, },
// @ts-ignore
$production: {
routeRules: {
'/api/_content/**': { isr: true, static: true },
'/api/component-meta/**': { isr: true, static: true }
}
},
routeRules: { routeRules: {
// '/getting-started': { swr: 100000 } '/': { redirect: '/getting-started' }
},
generate: {
routes: ['/getting-started']
},
componentMeta: {
metaFields: {
props: true,
slots: false,
events: false,
exposed: false
}
} }
}) })

View File

@@ -1,7 +0,0 @@
<template>
<div />
</template>
<script setup lang="ts">
await navigateTo('/getting-started')
</script>

View File

@@ -50,7 +50,7 @@ export default <Partial<Config>> {
borderRadius: '0.375rem', borderRadius: '0.375rem',
border: '1px solid var(--tw-prose-pre-border)', border: '1px solid var(--tw-prose-pre-border)',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-words' wordBreak: 'break-word'
}, },
code: { code: {
backgroundColor: 'var(--tw-prose-pre-bg)', backgroundColor: 'var(--tw-prose-pre-bg)',

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nuxthq/ui", "name": "@nuxthq/ui",
"version": "2.1.0", "version": "2.3.0",
"repository": "https://github.com/nuxtlabs/ui", "repository": "https://github.com/nuxtlabs/ui",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
@@ -19,22 +19,22 @@
}, },
"scripts": { "scripts": {
"build": "nuxt-module-build", "build": "nuxt-module-build",
"prepack": "yarn build", "prepack": "pnpm build",
"dev": "nuxi dev docs", "dev": "nuxi dev docs",
"build:docs": "nuxi build docs", "build:docs": "nuxi generate docs",
"lint": "eslint .", "lint": "eslint .",
"typecheck": "nuxi typecheck", "typecheck": "nuxi typecheck",
"prepare": "nuxi prepare docs", "prepare": "nuxi prepare docs",
"release": "yarn lint && standard-version && git push --follow-tags" "release": "pnpm lint && standard-version && git push --follow-tags"
}, },
"dependencies": { "dependencies": {
"@egoist/tailwindcss-icons": "^1.0.7", "@egoist/tailwindcss-icons": "^1.0.7",
"@headlessui/vue": "1.7.10", "@headlessui/vue": "1.7.10",
"@iconify-json/heroicons": "^1.1.10", "@iconify-json/heroicons": "^1.1.10",
"@nuxt/kit": "^3.4.3", "@nuxt/kit": "^3.5.2",
"@nuxtjs/color-mode": "^3.2.0", "@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.7.0", "@nuxtjs/tailwindcss": "^6.7.0",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.8",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
@@ -47,19 +47,19 @@
"tailwindcss": "^3.3.2" "tailwindcss": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/simple-icons": "^1.1.53", "@iconify-json/simple-icons": "^1.1.55",
"@nuxt/content": "^2.6.0", "@nuxt/content": "^2.6.0",
"@nuxt/devtools": "^0.4.6", "@nuxt/devtools": "^0.5.5",
"@nuxt/eslint-config": "^0.1.1", "@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.3.1", "@nuxt/module-builder": "^0.4.0",
"@nuxthq/studio": "^0.12.1", "@nuxthq/studio": "^0.13.2",
"@nuxtjs/plausible": "^0.2.1", "@nuxtjs/plausible": "^0.2.1",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",
"@types/node": "^20.1.7", "@types/node": "^20.2.5",
"@vueuse/nuxt": "^10.1.2", "@vueuse/nuxt": "^10.1.2",
"eslint": "^8.40.0", "eslint": "^8.41.0",
"nuxt": "^3.4.3", "nuxt": "^3.5.2",
"nuxt-component-meta": "^0.5.1", "nuxt-component-meta": "^0.5.3",
"nuxt-lodash": "^2.4.1", "nuxt-lodash": "^2.4.1",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"unbuild": "^1.2.1", "unbuild": "^1.2.1",

10154
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
# Restore all git changes # Restore all git changes
git restore -s@ -SW -- example src test git restore -s@ -SW -- .
# Bump versions to edge # Bump versions to edge
yarn jiti ./scripts/bump-edge pnpm jiti ./scripts/bump-edge
# Resolve yarn # Resolve pnpm
yarn pnpm install
# Update token # Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then

View File

@@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
# Restore all git changes # Restore all git changes
git restore -s@ -SW -- example src test git restore -s@ -SW -- .
# Resolve yarn # Resolve pnpm
yarn pnpm install
# Update token # Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then

View File

@@ -121,45 +121,49 @@ export default defineNuxtModule<ModuleOptions>({
} }
tailwindConfig.safelist = tailwindConfig.safelist || [] tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...['bg-gray-400', { tailwindConfig.safelist.push(...[
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`) 'bg-gray-500',
}, { 'dark:bg-gray-400',
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`), {
variants: ['disabled'] pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['dark'] variants: ['disabled']
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark:hover'] variants: ['dark']
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:disabled'] variants: ['dark:hover']
}, { }, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['hover'] variants: ['dark:disabled']
}, { }, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`), pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['focus-visible'] variants: ['hover']
}, { }, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`), pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['dark:focus-visible'] variants: ['focus-visible']
}, { }, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`), pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['focus-visible'] variants: ['dark:focus-visible']
}, { }, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`), pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['dark', 'dark:focus-visible'] variants: ['focus', 'focus-visible']
}, { }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`), pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark'] variants: ['dark', 'dark:focus', 'dark:focus-visible']
}, { }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`), pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['hover'] variants: ['dark']
}, { }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`), pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover'] variants: ['dark:hover']
}]) }, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}
])
tailwindConfig.plugins = tailwindConfig.plugins || [] tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) })) tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))
@@ -199,6 +203,12 @@ export default defineNuxtModule<ModuleOptions>({
global: options.global, global: options.global,
watch: false watch: false
}) })
addComponentsDir({
path: resolve(runtimeDir, 'components', 'data'),
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({ addComponentsDir({
path: resolve(runtimeDir, 'components', 'layout'), path: resolve(runtimeDir, 'components', 'layout'),
prefix: options.prefix, prefix: options.prefix,

View File

@@ -1,32 +1,79 @@
// Data
const table = {
wrapper: 'relative',
base: 'min-w-full table-fixed',
divide: 'divide-y divide-gray-300 dark:divide-gray-700',
thead: '',
tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
tr: {
base: '',
selected: 'bg-gray-50 dark:bg-gray-800/50'
},
th: {
base: 'text-left',
padding: 'px-3 py-3.5',
color: 'text-gray-900 dark:text-white',
font: 'font-semibold',
size: 'text-sm'
},
td: {
base: 'whitespace-nowrap',
padding: 'px-3 py-4',
color: 'text-gray-500 dark:text-gray-400',
font: '',
size: 'text-sm'
},
emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
label: 'text-sm text-center text-gray-900 dark:text-white',
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
},
default: {
sortAscIcon: 'i-heroicons-bars-arrow-up-20-solid',
sortDescIcon: 'i-heroicons-bars-arrow-down-20-solid',
sortButton: {
icon: 'i-heroicons-arrows-up-down-20-solid',
trailing: true,
square: true,
color: 'gray',
variant: 'ghost',
class: '-m-1.5'
},
emptyState: {
icon: 'i-heroicons-circle-stack-20-solid',
label: 'No items.'
}
}
}
// Elements // Elements
const avatar = { const avatar = {
wrapper: 'relative inline-flex items-center justify-center', wrapper: 'relative inline-flex items-center justify-center',
background: 'bg-gray-100 dark:bg-gray-800', background: 'bg-gray-100 dark:bg-gray-800',
rounded: 'rounded-full', rounded: 'rounded-full',
placeholder: 'text-xs font-medium leading-none text-gray-900 dark:text-white truncate', placeholder: 'font-medium leading-none text-gray-900 dark:text-white truncate',
size: { size: {
'3xs': 'h-4 w-4 text-xs', '3xs': 'h-4 w-4 text-[8px]',
'2xs': 'h-5 w-5 text-xs', '2xs': 'h-5 w-5 text-[10px]',
xs: 'h-6 w-6 text-xs', xs: 'h-6 w-6 text-[11px]',
sm: 'h-8 w-8 text-sm', sm: 'h-8 w-8 text-xs',
md: 'h-10 w-10 text-md', md: 'h-10 w-10 text-sm',
lg: 'h-12 w-12 text-lg', lg: 'h-12 w-12 text-base',
xl: 'h-14 w-14 text-xl', xl: 'h-14 w-14 text-lg',
'2xl': 'h-16 w-16 text-2xl', '2xl': 'h-16 w-16 text-xl',
'3xl': 'h-20 w-20 text-3xl' '3xl': 'h-20 w-20 text-2xl'
}, },
chip: { chip: {
base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900', base: 'absolute block rounded-full ring-1 ring-white dark:ring-gray-900',
background: 'bg-{color}-500 dark:bg-{color}-400',
position: { position: {
'top-right': 'top-0 right-0', 'top-right': 'top-0 right-0',
'bottom-right': 'bottom-0 right-0', 'bottom-right': 'bottom-0 right-0',
'top-left': 'top-0 left-0', 'top-left': 'top-0 left-0',
'bottom-left': 'bottom-0 left-0' 'bottom-left': 'bottom-0 left-0'
}, },
variant: {
solid: 'bg-{color}-400'
},
size: { size: {
'3xs': 'h-1 w-1', '3xs': 'h-1 w-1',
'2xs': 'h-1 w-1', '2xs': 'h-1 w-1',
@@ -41,7 +88,7 @@ const avatar = {
}, },
default: { default: {
size: 'sm', size: 'sm',
chipVariant: 'solid', chipColor: null,
chipPosition: 'top-right' chipPosition: 'top-right'
} }
} }
@@ -62,6 +109,7 @@ const badge = {
md: 'text-sm px-2 py-1', md: 'text-sm px-2 py-1',
lg: 'text-sm px-2.5 py-1.5' lg: 'text-sm px-2.5 py-1.5'
}, },
color: {},
variant: { variant: {
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20' solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
}, },
@@ -101,11 +149,11 @@ const button = {
xl: 'px-4 py-3' xl: 'px-4 py-3'
}, },
square: { square: {
'2xs': 'p-[5px]', '2xs': 'p-1',
xs: 'p-1.5', xs: 'p-1.5',
sm: 'p-2', sm: 'p-1.5',
md: 'p-2', md: 'p-2',
lg: 'p-2.5', lg: 'p-2',
xl: 'p-3' xl: 'p-3'
}, },
color: { color: {
@@ -220,7 +268,9 @@ const kbd = {
const input = { const input = {
wrapper: 'relative', wrapper: 'relative',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none', base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
rounded: 'rounded-md',
placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
custom: '', custom: '',
size: { size: {
'2xs': 'text-xs', '2xs': 'text-xs',
@@ -266,13 +316,21 @@ const input = {
xl: 'pr-12' xl: 'pr-12'
} }
}, },
appearance: { color: {
white: 'border-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500', white: {
gray: 'border-0 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500', outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400',
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none placeholder:text-gray-400 dark:placeholder:text-gray-500' },
gray: {
outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400',
}
},
variant: {
outline: 'shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
none: 'bg-transparent focus:ring-0 focus:shadow-none'
}, },
icon: { icon: {
base: 'text-gray-400 dark:text-gray-500', base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
color: 'text-{color}-500 dark:text-{color}-400',
size: { size: {
'2xs': 'h-3.5 w-3.5', '2xs': 'h-3.5 w-3.5',
xs: 'h-4 w-4', xs: 'h-4 w-4',
@@ -282,7 +340,8 @@ const input = {
xl: 'h-6 w-6' xl: 'h-6 w-6'
}, },
leading: { leading: {
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none', wrapper: 'absolute inset-y-0 left-0 flex items-center',
pointer: 'pointer-events-none',
padding: { padding: {
'2xs': 'pl-2', '2xs': 'pl-2',
xs: 'pl-2.5', xs: 'pl-2.5',
@@ -293,7 +352,8 @@ const input = {
} }
}, },
trailing: { trailing: {
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none', wrapper: 'absolute inset-y-0 right-0 flex items-center',
pointer: 'pointer-events-none',
padding: { padding: {
'2xs': 'pr-2', '2xs': 'pr-2',
xs: 'pr-2.5', xs: 'pr-2.5',
@@ -306,41 +366,49 @@ const input = {
}, },
default: { default: {
size: 'sm', size: 'sm',
appearance: 'white', color: 'white',
variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid' loadingIcon: 'i-heroicons-arrow-path-20-solid'
} }
} }
const inputGroup = { const formGroup = {
wrapper: '', wrapper: '',
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200', label: {
labelWrapper: 'flex content-center justify-between', wrapper: 'flex content-center justify-between',
base: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
required: `after:content-['*'] after:ml-0.5 after:text-red-500 dark:after:text-red-400`
},
description: 'text-sm text-gray-500 dark:text-gray-400',
container: 'mt-1 relative', container: 'mt-1 relative',
required: 'text-red-500 dark:text-red-400 ml-0.5', hint: 'text-sm text-gray-500 dark:text-gray-400',
description: 'text-sm leading-5 text-gray-500 dark:text-gray-400', help: 'mt-2 text-sm text-gray-500 dark:text-gray-400',
hint: 'text-sm leading-5 text-gray-500 dark:text-gray-400', error: 'mt-2 text-sm text-red-500 dark:text-red-400'
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400'
} }
const textarea = { const textarea = {
...input, ...input,
default: { default: {
size: 'sm', size: 'sm',
appearance: 'white' color: 'white',
variant: 'outline',
} }
} }
const select = { const select = {
...input, ...input,
placeholder: 'text-gray-900 dark:text-white',
default: { default: {
size: 'sm', size: 'sm',
appearance: 'white', color: 'white',
variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid',
trailingIcon: 'i-heroicons-chevron-down-20-solid' trailingIcon: 'i-heroicons-chevron-down-20-solid'
} }
} }
const selectMenu = { const selectMenu = {
wrapper: 'relative inline-flex', wrapper: 'relative',
container: 'z-20', container: 'z-20',
width: 'w-full', width: 'w-full',
height: 'max-h-60', height: 'max-h-60',
@@ -396,7 +464,7 @@ const selectMenu = {
const radio = { const radio = {
wrapper: 'relative flex items-start', wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent', base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
label: 'font-medium text-gray-700 dark:text-gray-200', label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400', required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400' help: 'text-gray-500 dark:text-gray-400'
@@ -412,7 +480,7 @@ const toggle = {
active: 'bg-primary-500 dark:bg-primary-400', active: 'bg-primary-500 dark:bg-primary-400',
inactive: 'bg-gray-200 dark:bg-gray-700', inactive: 'bg-gray-200 dark:bg-gray-700',
container: { container: {
base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200', base: 'pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-900 shadow transform ring-0 transition ease-in-out duration-200',
active: 'translate-x-4', active: 'translate-x-4',
inactive: 'translate-x-0' inactive: 'translate-x-0'
}, },
@@ -422,6 +490,10 @@ const toggle = {
inactive: 'opacity-0 ease-out duration-100', inactive: 'opacity-0 ease-out duration-100',
on: 'h-3 w-3 text-primary-500 dark:text-primary-400', on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
off: 'h-3 w-3 text-gray-400 dark:text-gray-500' off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
},
default: {
onIcon: null,
offIcon: null
} }
} }
@@ -467,8 +539,13 @@ const skeleton = {
const verticalNavigation = { const verticalNavigation = {
wrapper: 'relative', wrapper: 'relative',
base: 'group flex items-center gap-2 text-sm font-medium rounded-md w-full relative focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75', base: 'group relative flex items-center gap-2 focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-1 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 before:absolute before:inset-px before:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
ring: 'focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
padding: 'px-3 py-1.5', padding: 'px-3 py-1.5',
width: 'w-full',
rounded: 'rounded-md',
font: 'font-medium',
size: 'text-sm',
active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800', active: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800',
inactive: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50', inactive: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50',
label: 'truncate relative', label: 'truncate relative',
@@ -482,9 +559,9 @@ const verticalNavigation = {
size: '3xs' size: '3xs'
}, },
badge: { badge: {
base: 'ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5', base: 'relative ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
active: 'bg-white dark:bg-gray-900', active: 'bg-white dark:bg-gray-900',
inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 group-hover:bg-white dark:group-hover:bg-gray-900' inactive: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white group-hover:bg-white dark:group-hover:bg-gray-900'
} }
} }
@@ -502,9 +579,9 @@ const commandPalette = {
size: 'h-4 w-4', size: 'h-4 w-4',
padding: 'pl-10' padding: 'pl-10'
}, },
close: 'absolute right-4' closeButton: 'absolute right-4'
}, },
empty: { emptyState: {
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14', wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
label: 'text-sm text-center text-gray-900 dark:text-white', label: 'text-sm text-center text-gray-900 dark:text-white',
queryLabel: 'text-sm text-center text-gray-900 dark:text-white', queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
@@ -545,12 +622,13 @@ const commandPalette = {
}, },
default: { default: {
icon: 'i-heroicons-magnifying-glass-20-solid', icon: 'i-heroicons-magnifying-glass-20-solid',
empty: { loadingIcon: 'i-heroicons-arrow-path-20-solid',
emptyState: {
icon: 'i-heroicons-magnifying-glass-20-solid', icon: 'i-heroicons-magnifying-glass-20-solid',
label: 'We couldn\'t find any items.', label: 'We couldn\'t find any items.',
queryLabel: 'We couldn\'t find any items with that term. Please try again.' queryLabel: 'We couldn\'t find any items with that term. Please try again.'
}, },
close: null, closeButton: null,
selectedIcon: 'i-heroicons-check-20-solid' selectedIcon: 'i-heroicons-check-20-solid'
} }
} }
@@ -694,10 +772,20 @@ const notification = {
background: 'bg-white dark:bg-gray-900', background: 'bg-white dark:bg-gray-900',
shadow: 'shadow-lg', shadow: 'shadow-lg',
rounded: 'rounded-lg', rounded: 'rounded-lg',
padding: 'p-4',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800', ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
icon: 'flex-shrink-0 w-5 h-5 text-gray-900 dark:text-white', icon: {
avatar: 'flex-shrink-0 pt-0.5', base: 'flex-shrink-0 w-5 h-5 text-gray-400 dark:text-gray-500',
progress: 'absolute bottom-0 left-0 right-0 h-1 bg-primary-500 dark:bg-primary-400', color: 'text-{color}-500 dark:text-{color}-400'
},
avatar: {
base: 'flex-shrink-0 self-center',
size: 'md'
},
progress: {
base: 'absolute bottom-0 left-0 right-0 h-1',
background: 'bg-{color}-500 dark:bg-{color}-400'
},
transition: { transition: {
enterActiveClass: 'transform ease-out duration-300 transition', enterActiveClass: 'transform ease-out duration-300 transition',
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2', enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
@@ -707,13 +795,15 @@ const notification = {
leaveToClass: 'opacity-0' leaveToClass: 'opacity-0'
}, },
default: { default: {
close: { color: 'primary',
icon: null,
closeButton: {
icon: 'i-heroicons-x-mark-20-solid', icon: 'i-heroicons-x-mark-20-solid',
color: 'gray', color: 'gray',
variant: 'link', variant: 'link',
padded: false padded: false
}, },
action: { actionButton: {
size: 'xs', size: 'xs',
color: 'white' color: 'white'
} }
@@ -729,6 +819,7 @@ const notifications = {
export default { export default {
ui: { ui: {
table,
avatar, avatar,
avatarGroup, avatarGroup,
badge, badge,
@@ -737,7 +828,7 @@ export default {
dropdown, dropdown,
kbd, kbd,
input, input,
inputGroup, formGroup,
textarea, textarea,
select, select,
selectMenu, selectMenu,

View File

@@ -0,0 +1,192 @@
<template>
<div :class="ui.wrapper">
<table :class="[ui.base, ui.divide]">
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" class="pl-4">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" />
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size]">
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton
v-if="column.sortable"
v-bind="{ ...ui.default.sortButton, ...sortButton }"
:icon="(!sort.column || sort.column !== column.key) ? sortButton.icon : sort.direction === 'asc' ? sortAscIcon : sortDescIcon"
:label="column[columnAttribute]"
@click="onSort(column)"
/>
<span v-else>{{ column[columnAttribute] }}</span>
</slot>
</th>
</tr>
</thead>
<tbody :class="ui.tbody">
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]">
<td v-if="modelValue" class="pl-4">
<UCheckbox v-model="selected" :value="row" />
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]">
<slot :name="`${column.key}-data`" :column="column" :row="row">
{{ row[column.key] }}
</slot>
</td>
</tr>
<tr v-if="emptyState && !rows.length">
<td :colspan="columns.length">
<div :class="ui.emptyState.wrapper">
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
<p :class="ui.emptyState.label">
{{ emptyState.label }}
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import { ref, computed, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue'
import { capitalize, orderBy } from 'lodash-es'
import { defu } from 'defu'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
function defaultComparator<T>(a: T, z: T): boolean {
return a === z
}
export default defineComponent({
props: {
modelValue: {
type: Array,
default: null
},
by: {
type: [String, Function],
default: () => defaultComparator
},
rows: {
type: Array as PropType<{ [key: string]: any }[]>,
default: () => []
},
columns: {
type: Array as PropType<{ key: string, sortable?: boolean, [key: string]: any }[]>,
default: null
},
columnAttribute: {
type: String,
default: 'label'
},
sort: {
type: Object as PropType<{ column: string, direction: 'asc' | 'desc' }>,
default: () => ({})
},
sortButton: {
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.table.default.sortButton
},
sortAscIcon: {
type: String,
default: () => appConfig.ui.table.default.sortAscIcon
},
sortDescIcon: {
type: String,
default: () => appConfig.ui.table.default.sortDescIcon
},
emptyState: {
type: Object as PropType<{ icon: string, label: string }>,
default: () => appConfig.ui.table.default.emptyState
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.table>>,
default: () => appConfig.ui.table
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defu({}, props.ui, appConfig.ui.table))
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: capitalize(key), sortable: false })))
const sort = ref(defu({}, props.sort, { column: null, direction: 'asc' }))
const rows = computed(() => {
if (!sort.value?.column) {
return props.rows
}
const { column, direction } = sort.value
return orderBy(props.rows, column, direction)
})
const selected = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const indeterminate = computed(() => selected.value && selected.value.length > 0 && selected.value.length < props.rows.length)
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
function compare (a: any, z: any) {
if (typeof props.by === 'string') {
const property = props.by as unknown as any
return a?.[property] === z?.[property]
}
return props.by(a, z)
}
function isSelected (row) {
if (!props.modelValue) {
return false
}
return selected.value.some((item) => compare(toRaw(item), toRaw(row)))
}
function onSort (column) {
if (sort.value.column === column.key) {
sort.value.direction = sort.value.direction === 'asc' ? 'desc' : 'asc'
} else {
sort.value = { column: column.key, direction: column.direction || 'asc' }
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
// eslint-disable-next-line vue/no-dupe-keys
sort,
// eslint-disable-next-line vue/no-dupe-keys
columns,
// eslint-disable-next-line vue/no-dupe-keys
rows,
selected,
indeterminate,
// eslint-disable-next-line vue/no-dupe-keys
emptyState,
isSelected,
onSort
}
}
})
</script>

View File

@@ -43,18 +43,11 @@ export default defineComponent({
}, },
chipColor: { chipColor: {
type: String, type: String,
default: null, default: () => appConfig.ui.avatar.default.chipColor,
validator (value: string) { validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value) return ['gray', ...appConfig.ui.colors].includes(value)
} }
}, },
chipVariant: {
type: String,
default: () => appConfig.ui.avatar.default.chipVariant,
validator (value: string) {
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
}
},
chipPosition: { chipPosition: {
type: String, type: String,
default: () => appConfig.ui.avatar.default.chipPosition, default: () => appConfig.ui.avatar.default.chipPosition,
@@ -94,7 +87,7 @@ export default defineComponent({
ui.value.chip.base, ui.value.chip.base,
ui.value.chip.size[props.size], ui.value.chip.size[props.size],
ui.value.chip.position[props.chipPosition], ui.value.chip.position[props.chipPosition],
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor) ui.value.chip.background.replaceAll('{color}', props.chipColor)
) )
}) })

View File

@@ -1,7 +1,7 @@
import { h, computed, defineComponent } from 'vue' import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { classNames } from '../../utils' import { classNames, getSlotsChildren } from '../../utils'
import Avatar from './Avatar.vue' import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
@@ -34,36 +34,25 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup)) const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => { const children = computed(() => getSlotsChildren(slots))
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
})
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max) const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
const clones = computed(() => children.value.map((node, index) => { const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {}
if (!props.max || (max.value && index < max.value)) { if (!props.max || (max.value && index < max.value)) {
if (props.size) { if (props.size) {
node.props.size = props.size vProps.size = props.size
} }
node.props.class = node.props.class || '' vProps.class = node.props.class || ''
node.props.class += ` ${classNames( vProps.class += ` ${classNames(
ui.value.ring, ui.value.ring,
ui.value.margin ui.value.margin
)}` )}`
return node return cloneVNode(node, vProps)
} }
if (max.value !== undefined && index === max.value) { if (max.value !== undefined && index === max.value) {

View File

@@ -29,14 +29,17 @@ export default defineComponent({
type: String, type: String,
default: () => appConfig.ui.badge.default.color, default: () => appConfig.ui.badge.default.color,
validator (value: string) { validator (value: string) {
return appConfig.ui.colors.includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.badge.color)].includes(value)
} }
}, },
variant: { variant: {
type: String, type: String,
default: () => appConfig.ui.badge.default.variant, default: () => appConfig.ui.badge.default.variant,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.badge.variant).includes(value) return [
...Object.keys(appConfig.ui.badge.variant),
...Object.values(appConfig.ui.badge.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
label: { label: {
@@ -55,12 +58,14 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge)) const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
const badgeClass = computed(() => { const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.font, ui.value.font,
ui.value.rounded, ui.value.rounded,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.variant[props.variant]?.replaceAll('{color}', props.color) variant?.replaceAll('{color}', props.color)
) )
}) })

View File

@@ -5,20 +5,26 @@
:aria-label="ariaLabel" :aria-label="ariaLabel"
v-bind="buttonProps" v-bind="buttonProps"
> >
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" /> <slot name="leading" :disabled="disabled" :loading="loading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot>
<slot> <slot>
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']"> <span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
{{ label }} {{ label }}
</span> </span>
</slot> </slot>
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</component> </component>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useSlots } from 'vue' import { computed, defineComponent, useSlots } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
@@ -32,7 +38,8 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
components: { components: {
UIcon UIcon,
NuxtLink
}, },
props: { props: {
type: { type: {
@@ -108,7 +115,7 @@ export default defineComponent({
default: false default: false
}, },
to: { to: {
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>, type: [String, Object] as PropType<string | RouteLocationRaw>,
default: null default: null
}, },
target: { target: {
@@ -142,7 +149,7 @@ export default defineComponent({
const buttonIs = computed(() => { const buttonIs = computed(() => {
if (props.to) { if (props.to) {
return NuxtLink return 'NuxtLink'
} }
return 'button' return 'button'

View File

@@ -1,6 +1,7 @@
import { h, computed, defineComponent } from 'vue' import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -28,20 +29,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup)) const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
const children = computed(() => { const children = computed(() => getSlotsChildren(slots))
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
})
const rounded = computed(() => ({ const rounded = computed(() => ({
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' }, 'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' },
@@ -56,28 +44,30 @@ export default defineComponent({
}[ui.value.rounded])) }[ui.value.rounded]))
const clones = computed(() => children.value.map((node, index) => { const clones = computed(() => children.value.map((node, index) => {
const vProps: any = {}
if (props.size) { if (props.size) {
node.props.size = props.size vProps.size = props.size
} }
node.props.class = node.props.class || '' vProps.class = node.props.class || ''
node.props.class += ' !shadow-none' vProps.class += ' !shadow-none'
node.props.ui = node.props.ui || {} vProps.ui = node.props.ui || {}
node.props.ui.rounded = '' vProps.ui.rounded = ''
if (index === 0) { if (index === 0) {
node.props.ui.rounded = rounded.value.left vProps.ui.rounded = rounded.value.left
} }
if (index > 0) { if (index > 0) {
node.props.class += ' -ml-px' vProps.class += ' -ml-px'
} }
if (index === children.value.length - 1) { if (index === children.value.length - 1) {
node.props.ui.rounded = rounded.value.right vProps.ui.rounded = rounded.value.right
} }
return node return cloneVNode(node, vProps)
})) }))
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value) return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)

View File

@@ -15,14 +15,13 @@
</slot> </slot>
</MenuButton> </MenuButton>
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver"> <div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" :style="containerStyle" @mouseover="onMouseOver">
<transition appear v-bind="ui.transition"> <transition appear v-bind="ui.transition">
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static> <MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding"> <div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled"> <MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
<Component <ULinkCustom
v-bind="omit(item, ['click'])" v-bind="omit(item, ['label', 'icon', 'iconClass', 'avatar', 'shortcuts', 'click'])"
:is="(item.to && NuxtLink) || (item.click && 'button') || 'div'"
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]" :class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
@click="item.click" @click="item.click"
> >
@@ -36,7 +35,7 @@
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd> <UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
</span> </span>
</slot> </slot>
</Component> </ULinkCustom>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@@ -48,17 +47,17 @@
<script lang="ts"> <script lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defineComponent, ref, computed, onMounted } from 'vue' import { defineComponent, ref, computed, onMounted } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue' import UKbd from '../elements/Kbd.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import { omit } from '../../utils' import { omit } from '../../utils'
import { usePopper } from '../../composables/usePopper' import { usePopper } from '../../composables/usePopper'
import type { Avatar as AvatarType } from '../../types/avatar' import type { Avatar } from '../../types/avatar'
import type { PopperOptions } from '../../types' import type { PopperOptions } from '../../types'
import { NuxtLink } from '#components'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -75,22 +74,23 @@ export default defineComponent({
MenuItem, MenuItem,
UIcon, UIcon,
UAvatar, UAvatar,
UKbd UKbd,
ULinkCustom
}, },
props: { props: {
items: { items: {
type: Array as PropType<{ type: Array as PropType<{
to?: RouteLocationNormalized to?: string | RouteLocationRaw
exact?: boolean exact?: boolean
label: string label: string
disabled?: boolean slot?: string
slot?: string icon?: string
icon?: string iconClass?: string
iconClass?: string avatar?: Partial<Avatar>
avatar?: Partial<AvatarType> shortcuts?: string[]
click?: Function disabled?: boolean
shortcuts?: string[] click?: Function
}[][]>, }[][]>,
default: () => [] default: () => []
}, },
mode: { mode: {
@@ -127,7 +127,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown)) const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions)) const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
const [trigger, container] = usePopper(popper.value) const [trigger, container] = usePopper(popper.value)
@@ -149,6 +149,12 @@ export default defineComponent({
}, 200) }, 200)
}) })
const containerStyle = computed(() => {
const offsetDistance = (props.popper as PopperOptions)?.offsetDistance || (ui.value.popper as PopperOptions)?.offsetDistance || 8
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
})
function onMouseOver () { function onMouseOver () {
if (props.mode !== 'hover' || !menuApi.value) { if (props.mode !== 'hover' || !menuApi.value) {
return return
@@ -194,10 +200,10 @@ export default defineComponent({
ui, ui,
trigger, trigger,
container, container,
containerStyle,
onMouseOver, onMouseOver,
onMouseLeave, onMouseLeave,
omit, omit
NuxtLink
} }
} }
}) })

View File

@@ -8,6 +8,8 @@
:required="required" :required="required"
:value="value" :value="value"
:disabled="disabled" :disabled="disabled"
:checked="checked"
:indeterminate="indeterminate"
type="checkbox" type="checkbox"
:class="[ui.base, ui.custom]" :class="[ui.base, ui.custom]"
@focus="$emit('focus', $event)" @focus="$emit('focus', $event)"
@@ -40,7 +42,7 @@ import appConfig from '#build/app.config'
export default defineComponent({ export default defineComponent({
props: { props: {
value: { value: {
type: [String, Number, Boolean], type: [String, Number, Boolean, Object],
default: null default: null
}, },
modelValue: { modelValue: {
@@ -55,6 +57,14 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
checked: {
type: Boolean,
default: false
},
indeterminate: {
type: Boolean,
default: false
},
help: { help: {
type: String, type: String,
default: null default: null

View File

@@ -0,0 +1,84 @@
import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
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
},
error: {
type: [String, Boolean],
default: null
},
hint: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
default: () => appConfig.ui.formGroup
}
},
setup (props, { slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
const children = computed(() => getSlotsChildren(slots))
const clones = computed(() => children.value.map((node) => {
const vProps: any = {}
if (props.error) {
vProps.oldColor = node.props.color
vProps.color = 'red'
} else {
vProps.color = vProps.oldColor
}
if (props.name) {
vProps.name = props.name
}
return cloneVNode(node, vProps)
}))
return () => h('div', { class: [ui.value.wrapper] }, [
props.label && h('div', { class: [ui.value.label.wrapper] }, [
h('label', { for: props.name, class: [ui.value.label.base, props.required && ui.value.label.required] }, props.label),
props.hint && h('span', { class: [ui.value.hint] }, props.hint)
]),
props.description && h('p', { class: [ui.value.description] }, props.description),
h('div', { class: [!!props.label && ui.value.container] }, [
...clones.value,
props.error && typeof props.error === 'string' ? h('p', { class: [ui.value.error] }, props.error) : props.help ? h('p', { class: [ui.value.help] }, props.help) : null
])
])
}
})

View File

@@ -18,12 +18,18 @@
@blur="$emit('blur', $event)" @blur="$emit('blur', $event)"
> >
<slot /> <slot />
<div v-if="isLeading && leadingIconName" :class="leadingIconClass">
<UIcon :name="leadingIconName" :class="iconClass" /> <span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
</div> <slot name="leading" :disabled="disabled" :loading="loading">
<div v-if="isTrailing && trailingIconName" :class="trailingIconClass"> <UIcon :name="leadingIconName" :class="leadingIconClass" />
<UIcon :name="trailingIconName" :class="iconClass" /> </slot>
</div> </span>
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span>
</div> </div>
</template> </template>
@@ -55,7 +61,7 @@ export default defineComponent({
}, },
name: { name: {
type: String, type: String,
required: true default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -113,6 +119,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.input.default.size, default: () => appConfig.ui.input.default.size,
@@ -120,11 +130,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.input.size).includes(value) return Object.keys(appConfig.ui.input.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.input.default.appearance, default: () => appConfig.ui.input.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.input.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.input.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.input.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.input.variant),
...Object.values(appConfig.ui.input.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
ui: { ui: {
@@ -133,7 +153,7 @@ export default defineComponent({
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) { setup (props, { emit, slots }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -158,13 +178,17 @@ export default defineComponent({
}) })
const inputClass = computed(() => { const inputClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.padding[props.size], props.padded && ui.value.padding[props.size],
ui.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
isLeading.value && ui.value.leading.padding[props.size], (isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
isTrailing.value && ui.value.trailing.padding[props.size], (isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size],
ui.value.custom ui.value.custom
) )
}) })
@@ -193,25 +217,37 @@ export default defineComponent({
return props.trailingIcon || props.icon return props.trailingIcon || props.icon
}) })
const iconClass = computed(() => { const leadingWrapperIconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.base, ui.value.icon.leading.wrapper,
ui.value.icon.size[props.size], ui.value.icon.leading.pointer,
props.loading && 'animate-spin' ui.value.icon.leading.padding[props.size]
) )
}) })
const leadingIconClass = computed(() => { const leadingIconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.leading.wrapper, ui.value.icon.base,
ui.value.icon.leading.padding[props.size] appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && 'animate-spin'
)
})
const trailingWrapperIconClass = computed(() => {
return classNames(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[props.size]
) )
}) })
const trailingIconClass = computed(() => { const trailingIconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.trailing.wrapper, ui.value.icon.base,
ui.value.icon.trailing.padding[props.size] appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
) )
}) })
@@ -222,11 +258,12 @@ export default defineComponent({
isLeading, isLeading,
isTrailing, isTrailing,
inputClass, inputClass,
iconClass,
leadingIconName, leadingIconName,
leadingIconClass, leadingIconClass,
leadingWrapperIconClass,
trailingIconName, trailingIconName,
trailingIconClass, trailingIconClass,
trailingWrapperIconClass,
onInput onInput
} }
} }

View File

@@ -1,78 +0,0 @@
<template>
<div :class="ui.wrapper">
<div v-if="label || $slots.label" :class="ui.labelWrapper">
<label :for="name" :class="ui.label">
<slot name="label">{{ label }}</slot>
<span v-if="required" :class="ui.required">*</span>
</label>
<span v-if="$slots.hint || hint" :class="ui.hint">
<slot name="hint">{{ hint }}</slot>
</span>
</div>
<p v-if="description" :class="ui.description">
{{ description }}
</p>
<div :class="!!label && ui.container">
<slot />
<p v-if="help" :class="ui.help">
{{ help }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
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
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.inputGroup>>,
default: () => appConfig.ui.inputGroup
}
},
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.inputGroup>>(() => defu({}, props.ui, appConfig.ui.inputGroup))
return {
// eslint-disable-next-line vue/no-dupe-keys
ui
}
}
})
</script>

View File

@@ -5,7 +5,7 @@
:name="name" :name="name"
:value="modelValue" :value="modelValue"
:required="required" :required="required"
:disabled="disabled" :disabled="disabled || loading"
:class="selectClass" :class="selectClass"
@input="onInput" @input="onInput"
> >
@@ -36,12 +36,16 @@
</template> </template>
</select> </select>
<div v-if="icon" :class="leadingIconClass"> <span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<UIcon :name="icon" :class="iconClass" /> <slot name="leading" :disabled="disabled" :loading="loading">
</div> <UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span v-if="trailingIcon" :class="trailingIconClass"> <span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<UIcon :name="trailingIcon" :class="iconClass" aria-hidden="true" /> <slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span> </span>
</div> </div>
</template> </template>
@@ -71,7 +75,7 @@ export default defineComponent({
}, },
name: { name: {
type: String, type: String,
required: true default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -89,10 +93,34 @@ export default defineComponent({
type: String, type: String,
default: null default: null
}, },
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: { trailingIcon: {
type: String, type: String,
default: () => appConfig.ui.select.default.trailingIcon default: () => appConfig.ui.select.default.trailingIcon
}, },
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
padded: {
type: Boolean,
default: true
},
options: { options: {
type: Array, type: Array,
default: () => [] default: () => []
@@ -104,11 +132,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value) return Object.keys(appConfig.ui.select.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.select.default.appearance, default: () => appConfig.ui.select.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
textAttribute: { textAttribute: {
@@ -125,7 +163,7 @@ export default defineComponent({
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) { setup (props, { emit, slots }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -188,35 +226,75 @@ export default defineComponent({
}) })
const selectClass = computed(() => { const selectClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.rounded,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.padding[props.size], props.padded && ui.value.padding[props.size],
ui.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
!!props.icon && ui.value.leading.padding[props.size], (isLeading.value || slots.leading) && ui.value.leading.padding[props.size],
ui.value.trailing.padding[props.size], (isTrailing.value || slots.trailing) && ui.value.trailing.padding[props.size],
ui.value.custom ui.value.custom
) )
}) })
const iconClass = computed(() => { 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 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 leadingWrapperIconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.base, ui.value.icon.leading.wrapper,
ui.value.icon.size[props.size] ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[props.size]
) )
}) })
const leadingIconClass = computed(() => { const leadingIconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.leading.wrapper, ui.value.icon.base,
ui.value.icon.leading.padding[props.size] appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && 'animate-spin'
)
})
const trailingWrapperIconClass = computed(() => {
return classNames(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[props.size]
) )
}) })
const trailingIconClass = computed(() => { const trailingIconClass = computed(() => {
return classNames( return classNames(
ui.value.icon.trailing.wrapper, ui.value.icon.base,
ui.value.icon.trailing.padding[props.size] appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
) )
}) })
@@ -225,10 +303,15 @@ export default defineComponent({
ui, ui,
normalizedOptionsWithPlaceholder, normalizedOptionsWithPlaceholder,
normalizedValue, normalizedValue,
isLeading,
isTrailing,
selectClass, selectClass,
iconClass, leadingIconName,
leadingIconClass, leadingIconClass,
leadingWrapperIconClass,
trailingIconName,
trailingIconClass, trailingIconClass,
trailingWrapperIconClass,
onInput onInput
} }
} }

View File

@@ -6,7 +6,7 @@
:name="name" :name="name"
:model-value="modelValue" :model-value="modelValue"
:multiple="multiple" :multiple="multiple"
:disabled="disabled" :disabled="disabled || loading"
as="div" as="div"
:class="ui.wrapper" :class="ui.wrapper"
@update:model-value="onUpdate" @update:model-value="onUpdate"
@@ -27,19 +27,24 @@
role="button" role="button"
class="inline-flex w-full" class="inline-flex w-full"
> >
<slot :open="open" :disabled="disabled"> <slot :open="open" :disabled="disabled" :loading="loading">
<button :class="selectMenuClass" :disabled="disabled" type="button"> <button :class="selectMenuClass" :disabled="disabled || loading" type="button">
<span v-if="icon" :class="leadingIconClass"> <span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<UIcon :name="icon" :class="iconClass" /> <slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span> </span>
<slot name="label"> <slot name="label">
<span v-if="modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span> <span v-if="multiple && Array.isArray(modelValue) && modelValue.length" class="block truncate">{{ modelValue.length }} selected</span>
<span v-else class="block truncate text-gray-400 dark:text-gray-500">{{ placeholder || '&nbsp;' }}</span> <span v-else-if="!multiple && modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : modelValue[optionAttribute] }}</span>
<span v-else class="block truncate" :class="ui.placeholder">{{ placeholder || '&nbsp;' }}</span>
</slot> </slot>
<span v-if="trailingIcon" :class="trailingIconClass"> <span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<UIcon :name="trailingIcon" :class="iconClass" aria-hidden="true" /> <slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span> </span>
</button> </button>
</slot> </slot>
@@ -53,7 +58,7 @@
ref="searchInput" ref="searchInput"
:display-value="() => query" :display-value="() => query"
name="q" name="q"
placeholder="Search..." :placeholder="searchablePlaceholder"
autofocus autofocus
autocomplete="off" autocomplete="off"
:class="ui.input" :class="ui.input"
@@ -166,10 +171,30 @@ export default defineComponent({
type: String, type: String,
default: null default: null
}, },
loadingIcon: {
type: String,
default: () => appConfig.ui.input.default.loadingIcon
},
leadingIcon: {
type: String,
default: null
},
trailingIcon: { trailingIcon: {
type: String, type: String,
default: () => appConfig.ui.select.default.trailingIcon default: () => appConfig.ui.select.default.trailingIcon
}, },
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
selectedIcon: { selectedIcon: {
type: String, type: String,
default: () => appConfig.ui.selectMenu.default.selectedIcon default: () => appConfig.ui.selectMenu.default.selectedIcon
@@ -186,6 +211,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
searchablePlaceholder: {
type: String,
default: 'Search...'
},
creatable: { creatable: {
type: Boolean, type: Boolean,
default: false default: false
@@ -194,6 +223,10 @@ export default defineComponent({
type: String, type: String,
default: null default: null
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.select.default.size, default: () => appConfig.ui.select.default.size,
@@ -201,11 +234,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value) return Object.keys(appConfig.ui.select.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.select.default.appearance, default: () => appConfig.ui.select.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
optionAttribute: { optionAttribute: {
@@ -230,7 +273,7 @@ export default defineComponent({
} }
}, },
emits: ['update:modelValue', 'open', 'close'], emits: ['update:modelValue', 'open', 'close'],
setup (props, { emit }) { setup (props, { emit, slots }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -245,38 +288,78 @@ export default defineComponent({
const searchInput = ref<ComponentPublicInstance<HTMLElement>>() const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const selectMenuClass = computed(() => { const selectMenuClass = computed(() => {
const variant = uiSelect.value.color?.[props.color as string]?.[props.variant as string] || uiSelect.value.variant[props.variant]
return classNames( return classNames(
uiSelect.value.base, uiSelect.value.base,
uiSelect.value.rounded,
'text-left cursor-default', 'text-left cursor-default',
uiSelect.value.size[props.size], uiSelect.value.size[props.size],
uiSelect.value.gap[props.size], uiSelect.value.gap[props.size],
uiSelect.value.padding[props.size], props.padded && uiSelect.value.padding[props.size],
uiSelect.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
!!props.icon && uiSelect.value.leading.padding[props.size], (isLeading.value || slots.leading) && uiSelect.value.leading.padding[props.size],
uiSelect.value.trailing.padding[props.size], (isTrailing.value || slots.trailing) && uiSelect.value.trailing.padding[props.size],
uiSelect.value.custom, uiSelect.value.custom,
'inline-flex items-center' 'inline-flex items-center'
) )
}) })
const iconClass = computed(() => { 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 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 leadingWrapperIconClass = computed(() => {
return classNames( return classNames(
uiSelect.value.icon.base, uiSelect.value.icon.leading.wrapper,
uiSelect.value.icon.size[props.size] uiSelect.value.icon.leading.pointer,
uiSelect.value.icon.leading.padding[props.size]
) )
}) })
const leadingIconClass = computed(() => { const leadingIconClass = computed(() => {
return classNames( return classNames(
uiSelect.value.icon.leading.wrapper, uiSelect.value.icon.base,
uiSelect.value.icon.leading.padding[props.size] appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size],
props.loading && 'animate-spin'
)
})
const trailingWrapperIconClass = computed(() => {
return classNames(
uiSelect.value.icon.trailing.wrapper,
uiSelect.value.icon.trailing.pointer,
uiSelect.value.icon.trailing.padding[props.size]
) )
}) })
const trailingIconClass = computed(() => { const trailingIconClass = computed(() => {
return classNames( return classNames(
uiSelect.value.icon.trailing.wrapper, uiSelect.value.icon.base,
uiSelect.value.icon.trailing.padding[props.size] appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
) )
}) })
@@ -316,10 +399,15 @@ export default defineComponent({
ui, ui,
trigger, trigger,
container, container,
isLeading,
isTrailing,
selectMenuClass, selectMenuClass,
iconClass, leadingIconName,
leadingIconClass, leadingIconClass,
leadingWrapperIconClass,
trailingIconName,
trailingIconClass, trailingIconClass,
trailingWrapperIconClass,
filteredOptions, filteredOptions,
queryOption, queryOption,
query, query,

View File

@@ -38,7 +38,7 @@ export default defineComponent({
}, },
name: { name: {
type: String, type: String,
required: true default: null
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -72,6 +72,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false default: false
}, },
padded: {
type: Boolean,
default: true
},
size: { size: {
type: String, type: String,
default: () => appConfig.ui.textarea.default.size, default: () => appConfig.ui.textarea.default.size,
@@ -79,11 +83,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.textarea.size).includes(value) return Object.keys(appConfig.ui.textarea.size).includes(value)
} }
}, },
appearance: { color: {
type: String, type: String,
default: () => appConfig.ui.textarea.default.appearance, default: () => appConfig.ui.textarea.default.color,
validator (value: string) { validator (value: string) {
return Object.keys(appConfig.ui.textarea.appearance).includes(value) return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.textarea.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.textarea.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.textarea.variant),
...Object.values(appConfig.ui.textarea.color).flatMap(value => Object.keys(value))
].includes(value)
} }
}, },
ui: { ui: {
@@ -146,11 +160,15 @@ export default defineComponent({
}) })
const textareaClass = computed(() => { const textareaClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames( return classNames(
ui.value.base, ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size], ui.value.size[props.size],
ui.value.padding[props.size], props.padded && ui.value.padding[props.size],
ui.value.appearance[props.appearance], variant?.replaceAll('{color}', props.color),
!props.resize && 'resize-none', !props.resize && 'resize-none',
ui.value.custom ui.value.custom
) )

View File

@@ -1,14 +1,15 @@
<template> <template>
<Switch <Switch
v-model="active" v-model="active"
:name="name"
:class="[active ? ui.active : ui.inactive, ui.base]" :class="[active ? ui.active : ui.inactive, ui.base]"
> >
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]"> <span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
<span v-if="iconOn" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true"> <span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
<UIcon :name="iconOn" :class="ui.icon.on" /> <UIcon :name="onIcon" :class="ui.icon.on" />
</span> </span>
<span v-if="iconOff" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true"> <span v-if="offIcon" :class="[active ? ui.icon.inactive : ui.icon.active, ui.icon.base]" aria-hidden="true">
<UIcon :name="iconOff" :class="ui.icon.off" /> <UIcon :name="offIcon" :class="ui.icon.off" />
</span> </span>
</span> </span>
</Switch> </Switch>
@@ -34,17 +35,21 @@ export default defineComponent({
UIcon UIcon
}, },
props: { props: {
name: {
type: String,
default: null
},
modelValue: { modelValue: {
type: Boolean, type: Boolean,
default: false default: false
}, },
iconOn: { onIcon: {
type: String, type: String,
default: null default: () => appConfig.ui.toggle.default.onIcon
}, },
iconOff: { offIcon: {
type: String, type: String,
default: null default: () => appConfig.ui.toggle.default.offIcon
}, },
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>, type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,

View File

@@ -8,7 +8,7 @@
> >
<div :class="ui.wrapper"> <div :class="ui.wrapper">
<div v-show="searchable" :class="ui.input.wrapper"> <div v-show="searchable" :class="ui.input.wrapper">
<UIcon v-if="icon" :name="icon" :class="[ui.input.icon.base, ui.input.icon.size]" aria-hidden="true" /> <UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" />
<ComboboxInput <ComboboxInput
ref="comboboxInput" ref="comboboxInput"
:value="query" :value="query"
@@ -19,9 +19,9 @@
/> />
<UButton <UButton
v-if="close" v-if="closeButton"
v-bind="close" v-bind="{ ...ui.default.closeButton, ...closeButton }"
:class="ui.input.close" :class="ui.input.closeButton"
aria-label="Close" aria-label="Close"
@click="onClear" @click="onClear"
/> />
@@ -51,10 +51,10 @@
</CommandPaletteGroup> </CommandPaletteGroup>
</ComboboxOptions> </ComboboxOptions>
<div v-else-if="empty" :class="ui.empty.wrapper"> <div v-else-if="emptyState" :class="ui.emptyState.wrapper">
<UIcon v-if="empty.icon" :name="empty.icon" :class="ui.empty.icon" aria-hidden="true" /> <UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
<p :class="query ? ui.empty.queryLabel : ui.empty.label"> <p :class="query ? ui.emptyState.queryLabel : ui.emptyState.label">
{{ query ? empty.queryLabel : empty.label }} {{ query ? emptyState.queryLabel : emptyState.label }}
</p> </p>
</div> </div>
</div> </div>
@@ -73,7 +73,8 @@ import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { Group, Command } from '../../types/command-palette' import type { Group, Command } from '../../types/command-palette'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import type { Button as ButtonType } from '../../types/button' import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import CommandPaletteGroup from './CommandPaletteGroup.vue' import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
@@ -112,6 +113,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true default: true
}, },
loading: {
type: Boolean,
default: false
},
groups: { groups: {
type: Array as PropType<Group[]>, type: Array as PropType<Group[]>,
default: () => [] default: () => []
@@ -120,17 +125,21 @@ export default defineComponent({
type: String, type: String,
default: () => appConfig.ui.commandPalette.default.icon default: () => appConfig.ui.commandPalette.default.icon
}, },
loadingIcon: {
type: String,
default: () => appConfig.ui.commandPalette.default.loadingIcon
},
selectedIcon: { selectedIcon: {
type: String, type: String,
default: () => appConfig.ui.commandPalette.default.selectedIcon default: () => appConfig.ui.commandPalette.default.selectedIcon
}, },
close: { closeButton: {
type: Object as PropType<Partial<ButtonType>>, type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.commandPalette.default.close default: () => appConfig.ui.commandPalette.default.closeButton
}, },
empty: { emptyState: {
type: Object as PropType<{ icon: string, label: string, queryLabel: string }>, type: Object as PropType<{ icon: string, label: string, queryLabel: string }>,
default: () => appConfig.ui.commandPalette.default.empty default: () => appConfig.ui.commandPalette.default.emptyState
}, },
placeholder: { placeholder: {
type: String, type: String,
@@ -175,6 +184,7 @@ export default defineComponent({
const query = ref('') const query = ref('')
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>() const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
const comboboxApi = ref(null) const comboboxApi = ref(null)
const isLoading = ref(false)
onMounted(() => { onMounted(() => {
if (props.autoselect) { if (props.autoselect) {
@@ -231,10 +241,17 @@ export default defineComponent({
const debouncedSearch = useDebounceFn(async () => { const debouncedSearch = useDebounceFn(async () => {
const searchableGroups = props.groups.filter(group => !!group.search) const searchableGroups = props.groups.filter(group => !!group.search)
if (!searchableGroups.length) {
return
}
isLoading.value = true
await Promise.all(searchableGroups.map(async (group) => { await Promise.all(searchableGroups.map(async (group) => {
searchResults.value[group.key] = await group.search(query.value) searchResults.value[group.key] = await group.search(query.value)
})) }))
isLoading.value = false
}, props.debounce) }, props.debounce)
watch(query, () => { watch(query, () => {
@@ -247,6 +264,24 @@ export default defineComponent({
}, 0) }, 0)
}) })
const iconName = computed(() => {
if ((props.loading || isLoading.value) && props.loadingIcon) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
ui.value.input.icon.base,
ui.value.input.icon.size,
((props.loading || isLoading.value) && props.loadingIcon) && 'animate-spin'
)
})
const emptyState = computed(() => ({ ...ui.value.default.emptyState, ...props.emptyState }))
// Methods // Methods
function activateFirstOption () { function activateFirstOption () {
@@ -292,6 +327,10 @@ export default defineComponent({
groups, groups,
comboboxInput, comboboxInput,
query, query,
iconName,
iconClass,
// eslint-disable-next-line vue/no-dupe-keys
emptyState,
onSelect, onSelect,
onClear onClear
} }

View File

@@ -4,11 +4,11 @@
v-for="(link, index) of links" v-for="(link, index) of links"
v-slot="{ isActive }" v-slot="{ isActive }"
:key="index" :key="index"
v-bind="link" v-bind="omit(link, ['label', 'icon', 'iconClass', 'avatar', 'badge', 'click'])"
:class="[ui.base, ui.padding]" :class="[ui.base, ui.padding, ui.width, ui.ring, ui.rounded, ui.font, ui.size]"
:active-class="ui.active" :active-class="ui.active"
:inactive-class="ui.inactive" :inactive-class="ui.inactive"
@click="link.click && link.click()" @click="link.click"
@keyup.enter="$event.target.blur()" @keyup.enter="$event.target.blur()"
> >
<slot name="avatar" :link="link"> <slot name="avatar" :link="link">
@@ -29,7 +29,7 @@
<span v-if="link.label" :class="ui.label">{{ link.label }}</span> <span v-if="link.label" :class="ui.label">{{ link.label }}</span>
</slot> </slot>
<slot name="badge" :link="link" :is-active="isActive"> <slot name="badge" :link="link" :is-active="isActive">
<span v-if="link.badge" :class="[ui.badge.baseClass, isActive ? ui.badge.active : ui.badge.inactive]"> <span v-if="link.badge" :class="[ui.badge.base, isActive ? ui.badge.active : ui.badge.inactive]">
{{ link.badge }} {{ link.badge }}
</span> </span>
</slot> </slot>
@@ -40,12 +40,13 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { defu } from 'defu' import { defu } from 'defu'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import ULinkCustom from '../elements/LinkCustom.vue' import ULinkCustom from '../elements/LinkCustom.vue'
import type { Avatar as AvatarType } from '../../types/avatar' import { omit } from '../../utils'
import type { Avatar } from '../../types/avatar'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -62,15 +63,15 @@ export default defineComponent({
props: { props: {
links: { links: {
type: Array as PropType<{ type: Array as PropType<{
to?: RouteLocationNormalized | string to?: string | RouteLocationRaw
exact?: boolean exact?: boolean
label: string label: string
icon?: string icon?: string
iconClass?: string iconClass?: string
avatar?: Partial<AvatarType> avatar?: Partial<Avatar>
click?: Function click?: Function
badge?: string badge?: string | number
}[]>, }[]>,
default: () => [] default: () => []
}, },
ui: { ui: {
@@ -86,7 +87,8 @@ export default defineComponent({
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui ui,
omit
} }
} }
}) })

View File

@@ -1,15 +1,11 @@
<template> <template>
<transition appear v-bind="ui.transition"> <transition appear v-bind="ui.transition">
<div <div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
:class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<div :class="[ui.container, ui.rounded, ui.ring]"> <div :class="[ui.container, ui.rounded, ui.ring]">
<div class="p-4"> <div :class="ui.padding">
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }"> <div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
<UIcon v-if="icon" :name="icon" :class="ui.icon" /> <UIcon v-if="icon" :name="icon" :class="iconClass" />
<UAvatar v-if="avatar" v-bind="avatar" :class="ui.avatar" /> <UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
<div class="w-0 flex-1"> <div class="w-0 flex-1">
<p :class="ui.title"> <p :class="ui.title">
@@ -20,19 +16,19 @@
</p> </p>
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2"> <div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" /> <UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
</div> </div>
</div> </div>
<div class="flex-shrink-0 flex items-center gap-3"> <div class="flex-shrink-0 flex items-center gap-3">
<div v-if="!description && actions.length" class="flex items-center gap-2"> <div v-if="!description && actions.length" class="flex items-center gap-2">
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" /> <UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
</div> </div>
<UButton v-if="close" v-bind="{ ...ui.default.close, ...close }" @click.stop="onClose" /> <UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="onClose" />
</div> </div>
</div> </div>
</div> </div>
<div v-if="timeout" :class="ui.progress" :style="progressBarStyle" /> <div v-if="timeout" :class="progressClass" :style="progressStyle" />
</div> </div>
</div> </div>
</transition> </transition>
@@ -46,9 +42,10 @@ import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue' import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import { useTimer } from '../../composables/useTimer' import { useTimer } from '../../composables/useTimer'
import type { ToastNotificationAction } from '../../types' import type { NotificationAction } from '../../types'
import type { Avatar as AvatarType } from '../../types/avatar' import type { Avatar} from '../../types/avatar'
import type { Button as ButtonType } from '../../types/button' import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
// @ts-expect-error // @ts-expect-error
@@ -77,28 +74,35 @@ export default defineComponent({
}, },
icon: { icon: {
type: String, type: String,
default: null default: () => appConfig.ui.notification.default.icon
}, },
avatar: { avatar: {
type: Object as PropType<Partial<AvatarType>>, type: Object as PropType<Partial<Avatar>>,
default: null default: null
}, },
close: { closeButton: {
type: Object as PropType<Partial<ButtonType>>, type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.notification.default.close default: () => appConfig.ui.notification.default.closeButton
}, },
timeout: { timeout: {
type: Number, type: Number,
default: 5000 default: 5000
}, },
actions: { actions: {
type: Array as PropType<ToastNotificationAction[]>, type: Array as PropType<NotificationAction[]>,
default: () => [] default: () => []
}, },
callback: { callback: {
type: Function, type: Function,
default: null default: null
}, },
color: {
type: String,
default: () => appConfig.ui.notification.default.color,
validator (value: string) {
return ['gray', ...appConfig.ui.colors].includes(value)
}
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>, type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
default: () => appConfig.ui.notification default: () => appConfig.ui.notification
@@ -114,12 +118,26 @@ export default defineComponent({
let timer: any = null let timer: any = null
const remaining = ref(props.timeout) const remaining = ref(props.timeout)
const progressBarStyle = computed(() => { const progressStyle = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100 const remainingPercent = remaining.value / props.timeout * 100
return { width: `${remainingPercent || 0}%` } return { width: `${remainingPercent || 0}%` }
}) })
const progressClass = computed(() => {
return classNames(
ui.value.progress.base,
ui.value.progress.background?.replaceAll('{color}', props.color)
)
})
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color?.replaceAll('{color}', props.color)
)
})
function onMouseover () { function onMouseover () {
if (timer) { if (timer) {
timer.pause() timer.pause()
@@ -144,7 +162,7 @@ export default defineComponent({
emit('close') emit('close')
} }
function onAction (action: ToastNotificationAction) { function onAction (action: NotificationAction) {
if (timer) { if (timer) {
timer.stop() timer.stop()
} }
@@ -179,7 +197,9 @@ export default defineComponent({
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
progressBarStyle, progressStyle,
progressClass,
iconClass,
onMouseover, onMouseover,
onMouseleave, onMouseleave,
onClose, onClose,

View File

@@ -17,7 +17,7 @@
import { computed, defineComponent } from 'vue' import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defu } from 'defu' import { defu } from 'defu'
import type { ToastNotification } from '../../types' import type { Notification } from '../../types'
import { useToast } from '../../composables/useToast' import { useToast } from '../../composables/useToast'
import UNotification from './Notification.vue' import UNotification from './Notification.vue'
import { useState, useAppConfig } from '#imports' import { useState, useAppConfig } from '#imports'
@@ -44,7 +44,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications)) const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications))
const toast = useToast() const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => []) const notifications = useState<Notification[]>('notifications', () => [])
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys

View File

@@ -33,6 +33,9 @@ export const defineShortcuts = (config: ShortcutsConfig) => {
let shortcuts: Shortcut[] = [] let shortcuts: Shortcut[] = []
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
// Input autocomplete triggers a keydown event
if (!e.key) { return }
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key) const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
for (const shortcut of shortcuts) { for (const shortcut of shortcuts) {

View File

@@ -1,8 +1,8 @@
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import type { ToastNotification } from '../types/toast' import type { Notification } from '../types/notification'
import { useToast } from './useToast' import { useToast } from './useToast'
export function useCopyToClipboard (options: Partial<ToastNotification> = {}) { export function useCopyToClipboard (options: Partial<Notification> = {}) {
const { copy: copyToClipboard, isSupported } = useClipboard() const { copy: copyToClipboard, isSupported } = useClipboard()
const toast = useToast() const toast = useToast()

View File

@@ -1,5 +1,6 @@
import { createSharedComposable, useActiveElement } from '@vueuse/core' import { createSharedComposable, useActiveElement } from '@vueuse/core'
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import type {} from '@vueuse/shared'
export const _useShortcuts = () => { export const _useShortcuts = () => {
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/)) const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))

View File

@@ -1,25 +1,25 @@
import type { ToastNotification } from '../types' import type { Notification } from '../types'
import { useState } from '#imports' import { useState } from '#imports'
export function useToast () { export function useToast () {
const notifications = useState<ToastNotification[]>('notifications', () => []) const notifications = useState<Notification[]>('notifications', () => [])
function add (notification: Partial<ToastNotification>) { function add (notification: Partial<Notification>) {
const body = { const body = {
id: new Date().getTime().toString(), id: new Date().getTime().toString(),
...notification ...notification
} }
const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id) const index = notifications.value.findIndex((n: Notification) => n.id === body.id)
if (index === -1) { if (index === -1) {
notifications.value.push(body as ToastNotification) notifications.value.push(body as Notification)
} }
return body return body
} }
function remove (id: string) { function remove (id: string) {
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id) notifications.value = notifications.value.filter((n: Notification) => n.id !== id)
} }
return { return {

View File

@@ -28,7 +28,8 @@ ${Object.entries(gray || colors.cool).map(([key, value]) => `--color-gray-${key}
const headData: any = { const headData: any = {
style: [{ style: [{
innerHTML: () => root.value, innerHTML: () => root.value,
tagPriority: -2 tagPriority: -2,
id: 'nuxt-ui-colors'
}] }]
} }

View File

@@ -1,5 +1,6 @@
export * from './avatar' export * from './avatar'
export * from './button'
export * from './clipboard' export * from './clipboard'
export * from './command-palette' export * from './command-palette'
export * from './notification'
export * from './popper' export * from './popper'
export * from './toast'

22
src/runtime/types/notification.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
import type { Avatar } from './avatar'
import type { Button } from './button'
import appConfig from '#build/app.config'
export interface NotificationAction extends Partial<Button> {
click: Function
}
export interface Notification {
id: string
title: string
description: string
icon?: string
avatar?: Partial<Avatar>
closeButton?: Partial<Button>
timeout: number
actions?: NotificationAction[]
click?: Function
callback?: Function
color?: string
ui?: Partial<typeof appConfig.ui.notification>
}

View File

@@ -1,17 +0,0 @@
import type { Button } from './button'
export interface ToastNotificationAction extends Partial<Button> {
click: Function
}
export interface ToastNotification {
id: string
title: string
description: string
type: string
icon?: string
timeout: number
actions?: ToastNotificationAction[]
click?: Function
callback?: Function
}

View File

@@ -14,3 +14,18 @@ export const omit = (obj: object, keys: string[]) => {
Object.entries(obj).filter(([key]) => !keys.includes(key)) Object.entries(obj).filter(([key]) => !keys.includes(key))
) )
} }
export const getSlotsChildren = (slots: any) => {
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
}

9701
yarn.lock

File diff suppressed because it is too large Load Diff