---
applyTo: '**/*.vue'
---

###  General Backend API Calls

```vue
<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

```vue
<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:

```vue
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](./Resource.story.md) 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.

```vue
<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**

```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**

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

```js
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](./Resource.story.md) are available on this
object. Similarly, there are resources for `fetchOne`, `setValue`, `insert`,
`delete`, and `runDocMethod`.

```js
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

```vue
<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

1. `label` & `key` is required in column object.

2. `width` is optional and it is used to set column width in list

   1. If you need a column to be `3` times a default column then add `3`. if
      width is not mentioned default will be `1`
   2. You can also add custom width in px and rem e.g `300px` or `12rem`
   3. Combination of both can also be used.

3. `align` is also optional. You can change the alignment of the content in the
   column by setting it as.

   1. `start` or `left` (default)
   2. `center` or `middle`
   3. `end` or `right`

4. You can add more attributes which can be used to render custom column header
   items.

### Row

1. The row object must contain a unique_key which was mentioned in ListView
   `row-key`
2. 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 `label` attribute which holds the actual value to be shown

   ```
   row: {
   	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

1. If you want to route using router-link just add a `getRowRoute` function
   which returns a route object

   `getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } })`

2. if you need to do some action add a `onRowClick` event handler

   `onRowClick: (row) => console.log(row.label + ' was clicked')`

3. 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
4. showTooltip (Boolean) - if true, tooltip will be shown on hover of row -
   default is true
5. 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:**
<img width="1213" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/36fafcf5-45c6-43f0-acde-f64afe38b550">

**With custom action buttons:**
<img width="1212" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/55e751b2-df66-4ff0-b852-af463014463f">

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

<img width="629" alt="image" src="https://github.com/frappe/frappe-ui/assets/30859809/38dfa834-96a2-4ac5-ad4b-30b3e6871d3f">

```
<ListSelectBanner>
	<div>Custom Banner</div>
</ListSelectBanner>
```


### Badge Component

#### Story

```vue
<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

```vue
<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:

```vue
<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

```vue
<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

```vue
<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>
```
