36 KiB
applyTo
| applyTo |
|---|
| **/*.vue |
General Backend API Calls
<template>
<Button @click="todos.reload()" :loading="todos.loading"> Reload </Button>
<pre>{{ todos }}</pre>
</template>
<script setup>
import { createResource } from 'frappe-ui'
let todos = createResource({
url: '/api/method/frappe.client.get_list',
params: {
doctype: 'ToDo',
filters: {
allocated_to: 'faris@frappe.io',
},
},
})
todos.fetch() // GET request
// POST request
todos.submit({
"description": "New ToDo",
})
</script>
Document Resource
<script setup>
let todo = createDocumentResource({
// name of the doctype
doctype: 'ToDo',
// name of the record
name: '',
// define doc methods to use as resources
whitelistedMethods: {
sendEmail: 'send_email',
},
// the above configuration enables the following API
// todo.sendEmail.submit()
// events
// error can occur from failed request
onError(error) {},
// on successful response
onSuccess(data) {},
// transform data before setting it
transform(doc) {
doc.open = false
return doc
},
// other events
delete: {
onSuccess() {},
onError() {},
},
setValue: {
onSuccess() {},
onError() {},
},
})
</script>
Example API:
todo.doc // doc returned from request
todo.reload() // reload the doc
// update options
todo.update({
doctype: '',
name: ''
})
todo.get // doc resource
todos.get.loading // true when data is being fetched
todos.get.error // error that occurred from making the request
todos.get.promise // promise object of the request, can be awaited
// resource to set value(s) on the document
todos.setValue
todos.setValue.submit({
// field value pairs to set
status: 'Closed',
description: 'Updated description'
})
// same as setValue but debounced
todos.setValueDebounced
// will run once after 500ms
todos.setValueDebounced.submit({
description: 'Updated description'
})
// resource to delete the document
todos.delete
todos.delete.submit()
// if whitelistedMethods is defined
// you get a resource for each whitelisted method
todos.sendEmail
todos.sendEmail.submit
todos.sendEmail.loading
List Resource
List Resource is a wrapper on top of Resource for working with lists. This feature only works with a Frappe Framework backend as of now.
Usage
A list resource knows how to fetch records of a DocType from a Frappe Framework
backend so there is no need to specify the url. Instead you only define
doctype, fields, filters, etc. You also get methods like next(),
setValue(), etc.
<template>
<div class="space-y-4">
<div
class="flex items-center justify-between"
v-for="todo in todos.data"
:key="todo.name"
>
<div>
{{ todo.description }}
</div>
<Badge>{{ todo.status }}</Badge>
</div>
</div>
<Button @click="todos.next()"> Next Page </Button>
</template>
<script setup>
import { useList } from 'frappe-ui'
let todos = useList({
doctype: 'ToDo',
fields: ['name', 'description', 'status'],
orderBy: 'creation desc',
start: 0,
pageLength: 5,
})
todos.fetch()
</script>
Options API
You can also define resources if you are using Options API. You need to register
the resourcesPlugin first.
main.js
import { resourcesPlugin } from 'frappe-ui'
app.use(resourcesPlugin)
In your .vue file, you can declare all your resources under the resources key as
functions. The resource object will be available on this.$resources.[name]. In
the following example, this.$resources.todos is the resource object.
Component.vue
<script>
export default {
resources: {
todos() {
return {
type: 'list',
doctype: 'ToDo',
fields: ['name', 'description', 'status'],
orderBy: 'creation desc',
start: 0,
pageLength: 5,
auto: true,
}
},
},
}
</script>
List of Options and API
Here is the list of all options and APIs that are available on a list resource.
Options
let todos = uesList({
// name of the doctype
doctype: 'ToDo',
// list of fields
fields: ['name', 'description', 'status', ...],
// object of filters to apply
filters: {
status: 'Open'
},
// the order in which records must be sorted
orderBy: 'creation desc',
// index from which records should be fetched
// default value is 0
start: 0,
// number of records to fetch in a single request
// default value is 20
pageLength: 20,
// parent doctype when you are fetching records of a child doctype
parent: null,
// set to 1 to enable debugging of list query
debug: 0,
// cache key to cache the resource
// can be a string
cacheKey: 'todos',
// or an array that can be serialized
cacheKey: ['todos', 'faris@frappe.io'],
// default value for url is "frappe.client.get_list"
// specify url if you want to use a custom API method
url: 'todo_app.api.get_todos',
// make the first request automatically
auto: true,
// events
// error can occur from failed request
onError(error) {
},
// on successful response
onSuccess(data) {
},
// transform data before setting it
transform(data) {
for (let d of data) {
d.open = false
}
return data
},
// other events
fetchOne: {
onSuccess() {},
onError() {}
},
insert: {
onSuccess() {},
onError() {}
},
delete: {
onSuccess() {},
onError() {}
},
setValue: {
onSuccess() {},
onError() {}
},
runDocMethod: {
onSuccess() {},
onError() {}
},
})
API
A list resource is made up of multiple individual resources. In our running
example, the resource object that fetches the list is at todos.list. So all
the properties of a resource are available on this
object. Similarly, there are resources for fetchOne, setValue, insert,
delete, and runDocMethod.
let todos = useList({...})
todos.data // data returned from request
todos.originalData // response data before being transformed
todos.reload() // reload the existing list
todos.next() // fetch the next page
todos.hasNextPage // whether there is next page to fetch
// update list options
todos.update({
fields: ['*'],
filters: {
status: 'Closed'
}
})
todos.data // list resource
todos.loading // true when data is being fetched
todos.error // error that occurred from making the request
todos.promise // promise object of the request, can be awaited
// resource to fetch and update a single record in the list
todos.fetchOne
// pass the name of the record to fetch that record and update the list
todos.fetchOne.submit(name)
// resource to set value(s) for a single record in the list
todos.setValue
todos.setValue.submit({
// id of the record
name: '',
// field value pairs to set
status: 'Closed',
description: 'Updated description'
})
// resource to insert a new record in the list
todos.insert
todos.insert.submit({
description: 'New todo'
})
// resource to delete a single record
todos.delete
todos.delete.submit(name)
// resource to run a doc method
todos.runDocMethod
todos.runDocMethod.submit({
// name of the doc method
method: 'send_email',
// name of the record
name: '',
// params to pass to the method
email: 'test@example.com'
})
List View Component
Story
<script setup>
import { reactive, h, ref } from 'vue'
import Avatar from '../Avatar/Avatar.vue'
import Badge from '../Badge/Badge.vue'
import { Button } from '../Button'
import FeatherIcon from '../FeatherIcon.vue'
import ListHeader from './ListHeader.vue'
import ListHeaderItem from './ListHeaderItem.vue'
import ListRow from './ListRow.vue'
import ListRowItem from './ListRowItem.vue'
import ListRows from './ListRows.vue'
import ListSelectBanner from './ListSelectBanner.vue'
import ListView from './ListView.vue'
const state = reactive({
selectable: true,
showTooltip: true,
resizeColumn: true,
emptyState: {
title: 'No records found',
description: 'Create a new record to get started',
button: {
label: 'New Record',
variant: 'solid',
onClick: () => console.log('New Record'),
},
},
})
const simple_columns = reactive([
{
label: 'Name',
key: 'name',
width: 3,
getLabel: ({ row }) => row.name,
prefix: ({ row }) => {
return h(Avatar, {
shape: 'circle',
image: row.user_image,
size: 'sm',
})
},
},
{
label: 'Email',
key: 'email',
width: '200px',
},
{
label: 'Role',
key: 'role',
},
{
label: 'Status',
key: 'status',
},
])
const simple_rows = [
{
id: 1,
name: 'John Doe',
email: 'john@doe.com',
status: 'Active',
role: 'Developer',
user_image: 'https://avatars.githubusercontent.com/u/499550',
},
{
id: 2,
name: 'Jane Doe',
email: 'jane@doe.com',
status: 'Inactive',
role: 'HR',
user_image: 'https://avatars.githubusercontent.com/u/499120',
},
]
const group_columns = reactive([
{
label: 'Name',
key: 'name',
width: 3,
},
{
label: 'Email',
key: 'email',
width: '200px',
},
{
label: 'Role',
key: 'role',
},
{
label: 'Status',
key: 'status',
},
])
const grouped_rows = ref([
{
group: 'Developer',
collapsed: false,
rows: [
{
id: 2,
name: 'Gary Fox',
email: 'gary@fox.com',
status: 'Inactive',
role: 'Developer',
},
{
id: 6,
name: 'Emily Davis',
email: 'emily@davis.com',
status: 'Active',
role: 'Developer',
},
{
id: 9,
name: 'David Lee',
email: 'david@lee.com',
status: 'Inactive',
role: 'Developer',
},
],
},
{
group: 'Manager',
collapsed: false,
rows: [
{
id: 3,
name: 'John Doe',
email: 'john@doe.com',
status: 'Active',
role: 'Manager',
},
{
id: 8,
name: 'Sarah Wilson',
email: 'sarah@wilson.com',
status: 'Active',
role: 'Manager',
},
],
},
{
group: 'Designer',
collapsed: false,
rows: [
{
id: 4,
name: 'Alice Smith',
email: 'alice@smith.com',
status: 'Active',
role: 'Designer',
},
{
id: 10,
name: 'Olivia Taylor',
email: 'olivia@taylor.com',
status: 'Active',
role: 'Designer',
},
],
},
{
group: 'HR',
collapsed: false,
rows: [
{
id: 1,
name: 'Jane Mary',
email: 'jane@doe.com',
status: 'Inactive',
role: 'HR',
},
{
id: 7,
name: 'Michael Brown',
email: 'michael@brown.com',
status: 'Inactive',
role: 'HR',
},
{
id: 12,
name: 'Sophia Martinez',
email: 'sophia@martinez.com',
status: 'Active',
role: 'HR',
},
],
},
{
group: 'Tester',
collapsed: false,
rows: [
{
id: 5,
name: 'Bob Johnson',
email: 'bob@johnson.com',
status: 'Inactive',
role: 'Tester',
},
{
id: 11,
name: 'James Anderson',
email: 'james@anderson.com',
status: 'Inactive',
role: 'Tester',
},
],
},
])
const custom_columns = reactive([
{
label: 'Name',
key: 'name',
width: 3,
icon: 'user',
},
{
label: 'Email',
key: 'email',
width: '200px',
icon: 'at-sign',
},
{
label: 'Role',
key: 'role',
icon: 'users',
},
{
label: 'Status',
key: 'status',
icon: 'check-circle',
},
])
const custom_rows = [
{
id: 1,
name: {
label: 'John Doe',
image: 'https://avatars.githubusercontent.com/u/499550',
},
email: 'john@doe.com',
status: {
label: 'Active',
bg_color: 'bg-surface-green-3',
},
role: {
label: 'Developer',
color: 'green',
},
},
{
id: 2,
name: {
label: 'Jane Doe',
image: 'https://avatars.githubusercontent.com/u/499120',
},
email: 'jane@doe.com',
status: {
label: 'Inactive',
bg_color: 'bg-surface-red-5',
},
role: {
label: 'HR',
color: 'red',
},
},
]
</script>
<template>
<Story :layout="{ type: 'grid', width: '95%' }">
<Variant title="Simple List">
<ListView
class="h-[150px]"
:columns="simple_columns"
:rows="simple_rows"
:options="{
getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } }),
selectable: state.selectable,
showTooltip: state.showTooltip,
resizeColumn: state.resizeColumn,
}"
row-key="id"
/>
</Variant>
<Variant title="Custom List">
<ListView
class="h-[150px]"
:columns="custom_columns"
:rows="custom_rows"
:options="{
onRowClick: (row) => console.log(row),
selectable: state.selectable,
showTooltip: state.showTooltip,
resizeColumn: state.resizeColumn,
}"
row-key="id"
>
<ListHeader>
<ListHeaderItem
v-for="column in custom_columns"
:key="column.key"
:item="column"
>
<template #prefix="{ item }">
<FeatherIcon :name="item.icon" class="h-4 w-4" />
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow
v-for="row in custom_rows"
:key="row.id"
v-slot="{ column, item }"
:row="row"
>
<ListRowItem :item="item" :align="column.align">
<template #prefix>
<div
v-if="column.key == 'status'"
class="h-3 w-3 rounded-full"
:class="item.bg_color"
/>
<Avatar
v-if="column.key == 'name'"
:shape="'circle'"
:image="item.image"
size="sm"
/>
</template>
<Badge
v-if="column.key == 'role'"
variant="subtle"
:theme="item.color"
size="md"
:label="item.label"
/>
</ListRowItem>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll }">
<div class="flex gap-2">
<Button variant="ghost" label="Delete" />
<Button
variant="ghost"
label="Unselect all"
@click="unselectAll"
/>
</div>
</template>
</ListSelectBanner>
</ListView>
</Variant>
<Variant title="Grouped Rows">
<ListView
class="h-[250px]"
:columns="group_columns"
:rows="grouped_rows"
:options="{
getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } }),
selectable: state.selectable,
showTooltip: state.showTooltip,
resizeColumn: state.resizeColumn,
}"
row-key="id"
>
<template #group-header="{ group }">
<span class="text-base font-medium leading-6 text-ink-gray-9">
{{ group.group }} ({{ group.rows.length }})
</span>
</template>
</ListView>
</Variant>
<Variant title="Cell Slot">
<div>
<ListView
class="h-[250px]"
:columns="simple_columns"
:rows="simple_rows"
:options="{
selectable: state.selectable,
showTooltip: state.showTooltip,
resizeColumn: state.resizeColumn,
emptyState: state.emptyState,
}"
row-key="id"
>
<template #cell="{ item, row, column }">
<Badge v-if="column.key == 'status'">{{ item }}</Badge>
<span class="font-medium text-ink-gray-7" v-else>{{ item }}</span>
</template>
</ListView>
</div>
</Variant>
<Variant title="Empty List">
<div>
<ListView
class="h-[250px]"
:columns="simple_columns"
:rows="[]"
:options="{
selectable: state.selectable,
showTooltip: state.showTooltip,
resizeColumn: state.resizeColumn,
emptyState: state.emptyState,
}"
row-key="id"
/>
</div>
</Variant>
<template #controls>
<HstCheckbox v-model="state.selectable" title="Selectable" />
<HstCheckbox v-model="state.showTooltip" title="Show tooltip" />
<HstCheckbox v-model="state.resizeColumn" title="Resize Column" />
<!-- empty state config -->
<HstText
v-model="state.emptyState.title"
title="Empty Title"
placeholder="No records found"
/>
<HstText
v-model="state.emptyState.description"
title="Empty Description"
placeholder="Create a new record to get started"
/>
</template>
</Story>
</template>
Props
Row Key
row-key is a unique key which is used to identify each row in the list. It is
required to be passed in the row object.
Column
-
label&keyis required in column object. -
widthis optional and it is used to set column width in list- If you need a column to be
3times a default column then add3. if width is not mentioned default will be1 - You can also add custom width in px and rem e.g
300pxor12rem - Combination of both can also be used.
- If you need a column to be
-
alignis also optional. You can change the alignment of the content in the column by setting it as.startorleft(default)centerormiddleendorright
-
You can add more attributes which can be used to render custom column header items.
Row
-
The row object must contain a unique_key which was mentioned in ListView
row-key -
Then you can add the row fields as key value pairs and each field can be an object or a string (to handle custom rendering)
{ // unique_key 'id' id: 1, // row fields name: 'John Doe', age: 25, email: 'john@doe.com', }E.g field value as an object (to handle custom rendering), but make sure it has a
labelattribute which holds the actual value to be shownrow: { name: { label: 'John Doe', image: '/johndoe.jpg', }, age: 25, status: { label: 'Active', color: 'green' } }
Grouped Rows
To render grouped rows, you must provide rows in the following format:
[
{
group: 'Group Title 1',
collapsed: false,
rows: [
{id: 1, key1: value1, key2: value2, ...},
{id: 2, key1: value1, key2: value2, ...},
]
},
{
group: 'Group Title 2',
collapsed: false,
rows: [
{id: 3, key1: value1, key2: value2, ...},
{id: 4, key1: value1, key2: value2, ...},
]
},
]
Options
-
If you want to route using router-link just add a
getRowRoutefunction which returns a route objectgetRowRoute: (row) => ({ name: 'User', params: { userId: row.id } }) -
if you need to do some action add a
onRowClickevent handleronRowClick: (row) => console.log(row.label + ' was clicked') -
selectable (Boolean) - if true, checkbox will be shown in header and rows, to select/multiselect rows and perform some action on them - default is true
-
showTooltip (Boolean) - if true, tooltip will be shown on hover of row - default is true
-
resizeColumn (Boolean) - if true, column can be resized by dragging the resizer on the right side of the column header - default is false
Selection Banner (Will be shown when selectable (default is true) is true)
Without custom action buttons:
<ListSelectBanner>
<template #actions>
<div class="flex gap-2">
<Button variant="ghost" label="Delete" />
<Button variant="ghost" label="Edit" />
</div>
</template>
</ListSelectBanner>
You can also make your own custom selection banner
<ListSelectBanner>
<div>Custom Banner</div>
</ListSelectBanner>
Badge Component
Story
<script setup lang="ts">
import { reactive } from 'vue'
import Badge from './Badge.vue'
const state = reactive({
theme: 'gray',
size: 'sm',
label: 'Badge',
})
const variants = ['solid', 'subtle', 'outline', 'ghost']
const themes = ['gray', 'blue', 'green', 'orange', 'red']
const sizes = ['sm', 'md', 'lg']
</script>
<template>
<Story :layout="{ type: 'grid', width: 300 }">
<Variant v-for="variant in variants" :key="variant" :title="variant">
<Badge :variant="variant" v-bind="state">{{ state.label }}</Badge>
</Variant>
<template #controls>
<HstText v-model="state.label" title="Content" />
<HstSelect v-model="state.theme" :options="themes" title="Theme" />
<HstSelect v-model="state.size" :options="sizes" title="Size" />
</template>
</Story>
</template>
FormControl Component
Story
<script setup lang="ts">
import { reactive, ref } from 'vue'
import FormControl from './FormControl.vue'
import FeatherIcon from '../FeatherIcon.vue'
import { Avatar } from '../Avatar'
const state = reactive({
size: 'sm',
variant: 'subtle',
placeholder: 'Placeholder',
disabled: false,
label: 'Label',
})
const inputValue = ref('')
const selectValue = ref(null)
const autocompleteValue = ref(null)
const checkboxValue = ref(false)
const inputTypes = [
'text',
'number',
'email',
'date',
'password',
'search',
'textarea',
]
const sizes = ['sm', 'md', 'lg', 'xl']
const variants = ['subtle', 'outline']
</script>
<template>
<Story :layout="{ type: 'grid', width: 500 }">
<Variant
v-for="inputType in inputTypes"
:key="inputType"
:title="inputType"
>
<div class="p-2">
<FormControl :type="inputType" v-bind="state" v-model="inputValue" />
</div>
</Variant>
<Variant title="select">
<div class="p-2">
<FormControl
type="select"
:options="[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3' },
]"
v-bind="state"
v-model="selectValue"
/>
</div>
</Variant>
<Variant title="autocomplete">
<div class="p-2">
<FormControl
type="autocomplete"
:options="[
{ label: 'One', value: '1' },
{ label: 'Two', value: '2' },
{ label: 'Three', value: '3' },
]"
v-bind="state"
v-model="autocompleteValue"
/>
</div>
</Variant>
<Variant title="checkbox">
<div class="p-2">
<FormControl type="checkbox" v-bind="state" v-model="checkboxValue" />
</div>
</Variant>
<Variant title="prefix slot icon">
<div class="p-2">
<FormControl type="text" label="Label">
<template #prefix>
<FeatherIcon class="w-4" name="search" />
</template>
</FormControl>
</div>
</Variant>
<Variant title="suffix slot icon">
<div class="p-2">
<FormControl type="text" label="Label">
<template #suffix>
<FeatherIcon class="w-4" name="search" />
</template>
</FormControl>
</div>
</Variant>
<Variant title="prefix slot avatar">
<div class="p-2">
<FormControl type="text" label="Label">
<template #prefix>
<Avatar
size="sm"
image="https://avatars.githubusercontent.com/u/499550?s=60&v=4"
/>
</template>
</FormControl>
</div>
</Variant>
<template #controls>
<HstSelect v-model="state.variant" :options="variants" title="Variant" />
<HstSelect v-model="state.size" :options="sizes" title="Size" />
</template>
</Story>
</template>
Toasts
Example:
<script setup>
import { toast } from 'frappe-ui'
toast.success('Converted successfully')
toast.warning('Converted Pending')
toast.error('Conversion Failed')
</script>
Dropdown Component
The icon is name of a lucide icon.
Story
<script setup lang="ts">
import { Dropdown } from './index'
import { Button } from '../Button'
const actions = [
{
label: 'Edit',
icon: 'edit',
onClick: () => console.log('Edit clicked'),
},
{
label: 'Delete',
icon: 'trash-2',
theme: 'red',
onClick: () => console.log('Delete clicked'),
},
]
const groupedActions = [
{
group: 'Actions',
items: [
{
label: 'Edit',
icon: 'edit',
onClick: () => console.log('Edit clicked'),
},
{
label: 'Duplicate',
icon: 'copy',
onClick: () => console.log('Duplicate clicked'),
},
{
label: 'More Actions',
icon: 'more-horizontal',
submenu: [
{
label: 'Archive',
icon: 'archive',
onClick: () => console.log('Archive clicked'),
},
{
label: 'Export',
icon: 'download',
submenu: [
{
label: 'Export as PDF',
icon: 'file-text',
onClick: () => console.log('Export as PDF clicked'),
},
{
label: 'Export as CSV',
icon: 'file',
onClick: () => console.log('Export as CSV clicked'),
},
],
},
{
label: 'Share',
icon: 'share',
onClick: () => console.log('Share clicked'),
},
],
},
],
},
{
group: 'Danger',
items: [
{
label: 'Delete',
icon: 'trash-2',
theme: 'red',
onClick: () => console.log('Delete clicked'),
},
],
},
]
const submenuActions = [
{
label: 'New',
icon: 'plus',
submenu: [
{
group: 'Documents',
items: [
{
label: 'New Document',
icon: 'file-plus',
onClick: () => console.log('New Document clicked'),
},
{
label: 'New Template',
icon: 'file-text',
onClick: () => console.log('New Template clicked'),
},
{
label: 'Delete',
icon: 'trash-2',
theme: 'red',
onClick: () => console.log('Delete clicked'),
},
],
},
{
group: 'Organization',
items: [
{
label: 'New Folder',
icon: 'folder-plus',
onClick: () => console.log('New Folder clicked'),
},
{
label: 'New Project',
icon: 'briefcase',
onClick: () => console.log('New Project clicked'),
},
],
},
],
},
{
label: 'Edit',
icon: 'edit',
onClick: () => console.log('Edit clicked'),
},
{
label: 'Share',
icon: 'share',
submenu: [
{
label: 'Share with Link',
icon: 'link',
onClick: () => console.log('Share with Link clicked'),
},
{
label: 'Share with Email',
icon: 'mail',
onClick: () => console.log('Share with Email clicked'),
},
{
group: 'Advanced',
items: [
{
label: 'Share Settings',
icon: 'settings',
onClick: () => console.log('Share Settings clicked'),
},
{
label: 'Permission Management',
icon: 'shield',
onClick: () => console.log('Permission Management clicked'),
},
],
},
],
},
]
</script>
<template>
<Story title="Dropdown" :layout="{ type: 'grid', width: '200px' }">
<Variant title="Default">
<div class="asdf">
<Dropdown :options="actions" />
</div>
</Variant>
<Variant title="With Custom Button">
<Dropdown :options="actions">
<Button variant="solid">Custom Trigger</Button>
</Dropdown>
</Variant>
<Variant title="With Groups">
<Dropdown :options="groupedActions" />
</Variant>
<Variant title="Right Aligned">
<Dropdown :options="actions" placement="right" />
</Variant>
<Variant title="Center Aligned">
<Dropdown :options="actions" placement="center" />
</Variant>
<Variant title="With Submenus">
<Dropdown :options="submenuActions" />
</Variant>
<Variant title="With Nested Submenus">
<Dropdown :options="groupedActions" />
</Variant>
</Story>
</template>
Dialog Component
Story
<script setup lang="ts">
import { ref } from 'vue'
import Dialog from './Dialog.vue'
import { Button } from '../Button'
import { Dropdown } from '../Dropdown'
import LucideSettings from '~icons/lucide/settings'
import LucideStar from '~icons/lucide/star'
import LucideChevronDown from '~icons/lucide/chevron-down'
import { Autocomplete } from '../Autocomplete'
const dialog1 = ref(false)
const dialog2 = ref(false)
const dialog3 = ref(false)
const dialog4 = ref(false)
const dialog5 = ref(false)
const dialog6 = ref(false)
// Dropdown state
const selectedOption = ref('Option 1')
const autocompleteValue = ref({ label: '', value: '' })
const dropdownOptions = [
{
label: 'Option 1',
onClick: () => {
selectedOption.value = 'Option 1'
},
},
{
label: 'Option 2',
onClick: () => {
selectedOption.value = 'Option 2'
},
},
{
label: 'Option 3',
onClick: () => {
selectedOption.value = 'Option 3'
},
},
{
group: 'Advanced Options',
items: [
{
label: 'Advanced Option A',
icon: LucideSettings,
onClick: () => {
selectedOption.value = 'Advanced Option A'
},
},
{
label: 'Advanced Option B',
icon: LucideStar,
onClick: () => {
selectedOption.value = 'Advanced Option B'
},
},
],
},
]
const createPromise = (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(resolve, 2000)
})
}
</script>
<template>
<Story :layout="{ width: 500, type: 'grid' }">
<!-- 1. Basic Dialog with Actions -->
<Variant title="Basic Dialog with Actions" autoPropsDisabled>
<Button @click="dialog1 = true">Show Confirmation Dialog</Button>
<Dialog
:options="{
title: 'Confirm Action',
message: 'Are you sure you want to proceed with this action?',
size: 'lg',
icon: {
name: 'alert-triangle',
appearance: 'warning',
},
actions: [
{
label: 'Confirm',
variant: 'solid',
onClick: () => createPromise(),
},
],
}"
v-model="dialog1"
/>
</Variant>
<!-- 2. Custom Content with Slots -->
<Variant title="Custom Content with Slots" autoPropsDisabled>
<Button @click="dialog2 = true">Show Custom Dialog</Button>
<Dialog v-model="dialog2">
<template #body-title>
<h3 class="text-2xl font-semibold text-blue-600">
Custom Title with Styling
</h3>
</template>
<template #body-content>
<div class="space-y-4">
<p class="text-gray-700">
This dialog uses custom slots for flexible content layout.
</p>
<div class="bg-blue-50 p-4 rounded-lg">
<p class="text-blue-800">
You can put any content here including forms, lists, or other
components.
</p>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="flex justify-start flex-row-reverse gap-2">
<Button variant="solid" @click="close">Save Changes</Button>
<Button variant="outline" @click="close">Cancel</Button>
</div>
</template>
</Dialog>
</Variant>
<!-- 3. Different Sizes -->
<Variant title="Different Sizes" autoPropsDisabled>
<div class="space-x-2">
<Button @click="dialog3 = true">Small Dialog</Button>
<Button @click="dialog4 = true">Large Dialog</Button>
</div>
<!-- Small Dialog -->
<Dialog
:options="{
title: 'Small Dialog',
message: 'This is a small dialog.',
size: 'sm',
actions: [{ label: 'OK', variant: 'solid' }],
}"
v-model="dialog3"
/>
<!-- Large Dialog -->
<Dialog
:options="{
title: 'Large Dialog',
message: 'This is a large dialog with more space for content.',
size: '4xl',
actions: [{ label: 'OK', variant: 'solid' }],
}"
v-model="dialog4"
/>
</Variant>
<!-- 4. Disable Outside Click -->
<Variant title="Disable Outside Click to Close" autoPropsDisabled>
<Button @click="dialog5 = true">Show Modal Dialog</Button>
<Dialog
:options="{
title: 'Modal Dialog',
message:
'This dialog cannot be closed by clicking outside. Use the buttons or ESC key.',
actions: [{ label: 'Close', variant: 'solid' }],
}"
:disable-outside-click-to-close="true"
v-model="dialog5"
/>
</Variant>
<!-- 5. Dialog with Interactive Components -->
<Variant title="Dialog with Interactive Components" autoPropsDisabled>
<Button @click="dialog6 = true">Show Settings Dialog</Button>
<Dialog v-model="dialog6">
<template #body-title>
<h3 class="text-2xl font-semibold text-ink-gray-9">
Settings Dialog
</h3>
</template>
<template #body-content>
<div class="space-y-6 text-base">
<p class="text-gray-700">
This dialog contains interactive elements to test proper layering.
</p>
<Autocomplete
:options="[
{ label: 'Option A', value: 'A' },
{ label: 'Option B', value: 'B' },
{ label: 'Option C', value: 'C' },
]"
placeholder="Type to search..."
v-model="autocompleteValue"
/>
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700">
Select an option:
</label>
<Dropdown :options="dropdownOptions" placement="left">
<Button variant="outline">
{{ selectedOption }}
<template #suffix>
<LucideChevronDown class="h-4 w-4 text-gray-500" />
</template>
</Button>
</Dropdown>
</div>
<div class="bg-gray-50 text-p-sm p-4 text-ink-gray-6 rounded-lg">
<p><strong>Selected value:</strong> {{ selectedOption }}</p>
<p class="mt-1">
Interactive components should work properly within dialogs.
</p>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="flex space-x-2">
<Button variant="solid" @click="close">Save Settings</Button>
<Button variant="outline" @click="close">Cancel</Button>
</div>
</template>
</Dialog>
</Variant>
</Story>
</template>