Files
event-manager/.github/instructions/frappe-ui.instructions.md
T
xadm 786cbc724f
CI / Server (push) Has been cancelled
Linters / Frappe Linter (push) Has been cancelled
Linters / Vulnerable Dependency Check (push) Has been cancelled
UI Tests / Playwright E2E Tests (push) Has been cancelled
Initialize fork and rebrand app to event_manager
2026-05-11 09:56:57 +02:00

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

  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: image

With custom action buttons: image

<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

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