Export menu
The export button of a register. It opens on what is left of the quota this period, then one button per format. Nothing downloads here: the callback does.
import { ExportMenu } from '@/components/export-menu'
<ExportMenu used={3} quota={10} onExport={(format) => startExport(format)} /><script setup lang="ts">
import ExportMenu from '@/components/export-menu.vue'
</script>
<template>
<ExportMenu :used="3" :quota="10" @export="startExport" />
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
'use client'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { ArrowDownTrayIcon } from '@heroicons/react/16/solid'
import type React from 'react'
import { Button } from './button'
import { Meter } from './meter'
export type ExportFormat = { value: string; label: string }
const defaultFormats: ExportFormat[] = [
{ value: 'csv', label: 'Download CSV' },
{ value: 'json', label: 'Download JSON' },
]
export function ExportMenu({
used,
quota,
period = 'this month',
formats = defaultFormats,
onExport,
label = 'Export',
className,
}: {
used: number
quota: number
period?: string
formats?: ExportFormat[]
onExport: (format: string) => void
label?: React.ReactNode
className?: string
}) {
let exhausted = used >= quota
return (
<Popover>
<PopoverButton as={Button} outline className={className}>
<ArrowDownTrayIcon data-slot="icon" />
{label}
</PopoverButton>
<PopoverPanel
anchor={{ to: 'bottom end', gap: 8, padding: 8 }}
className="z-50 w-64 rounded-xl border border-zinc-950/10 bg-white p-4 shadow-lg outline-none dark:border-white/10 dark:bg-zinc-800"
>
{({ close }) => (
<>
<p className="text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
<span className="font-medium text-zinc-950 tabular-nums dark:text-white">
{used}/{quota}
</span>{' '}
exports {period}
</p>
<Meter className="mt-2" value={used} max={quota} label="Export quota" />
{exhausted && (
<p className="mt-2 text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
No exports left {period}.
</p>
)}
<div className="mt-4 flex flex-col gap-2">
{formats.map((format) => (
<Button
key={format.value}
outline
disabled={exhausted}
onClick={() => {
onExport(format.value)
close()
}}
>
{format.label}
</Button>
))}
</div>
</>
)}
</PopoverPanel>
</Popover>
)
}<script lang="ts">
export type ExportFormat = { value: string; label: string }
</script>
<script setup lang="ts">
import { ArrowDownTrayIcon } from '@heroicons/vue/16/solid'
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
import { computed, ref } from 'vue'
import Button from './button.vue'
import Meter from './meter.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
used: number
quota: number
period?: string
formats?: ExportFormat[]
label?: string
}>(),
{
period: 'this month',
formats: () => [
{ value: 'csv', label: 'Download CSV' },
{ value: 'json', label: 'Download JSON' },
],
label: 'Export',
}
)
const emit = defineEmits<{ export: [format: string] }>()
const open = ref(false)
const exhausted = computed(() => props.used >= props.quota)
function pick(format: string) {
emit('export', format)
open.value = false
}
</script>
<template>
<PopoverRoot v-model:open="open">
<PopoverTrigger as-child>
<Button outline v-bind="$attrs">
<ArrowDownTrayIcon data-slot="icon" />
<slot name="label">{{ label }}</slot>
</Button>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
align="end"
:side-offset="8"
:collision-padding="8"
class="z-50 w-64 rounded-xl border border-zinc-950/10 bg-white p-4 shadow-lg outline-none dark:border-white/10 dark:bg-zinc-800"
>
<p class="text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
<span class="font-medium text-zinc-950 tabular-nums dark:text-white">{{ used }}/{{ quota }}</span>
exports {{ period }}
</p>
<Meter class="mt-2" :value="used" :max="quota" label="Export quota" />
<p v-if="exhausted" class="mt-2 text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
No exports left {{ period }}.
</p>
<div class="mt-4 flex flex-col gap-2">
<Button v-for="format in formats" :key="format.value" outline :disabled="exhausted" @click="pick(format.value)">
{{ format.label }}
</Button>
</div>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>Component API
| Prop | Default | Description |
|---|---|---|
ExportMenubuilt on the Headless UI <Headless.Popover> component | ||
used | - | Exports already made this period. |
quota | - | Exports allowed per period. At the cap, the format buttons disable and a line says no exports are left. |
period | 'this month' | Named in the sentence: `3/10 exports this month`. |
formats | CSV and JSON | Array of `{ value, label }`, one button each. |
onExport | - | Called with the value of the format picked. The popover closes. |
label | 'Export' | Label of the trigger button. |
className | - | Classes of the trigger button. |
| Prop | Default | Description |
|---|---|---|
ExportMenubuilt on the Reka UI <PopoverRoot> component | ||
used | - | Exports already made this period. |
quota | - | Exports allowed per period. At the cap, the format buttons disable. |
period | 'this month' | Named in the sentence: `3/10 exports this month`. |
formats | CSV and JSON | Array of `{ value, label }`, one button each. |
@export | - | Emitted with the value of the format picked. The popover closes. |
label | 'Export' | Label of the trigger button. Use the `#label` slot for markup. |
Examples
Basic example
Pass what the account used and what it may use. The meter below the count says the same thing at a glance:
import { ExportMenu } from '@/components/export-menu'
<ExportMenu used={3} quota={10} onExport={(format) => startExport(format)} /><script setup lang="ts">
import ExportMenu from '@/components/export-menu.vue'
</script>
<template>
<ExportMenu :used="3" :quota="10" @export="startExport" />
</template>Quota reached
At the cap the buttons disable; the count says why:
<ExportMenu used={10} quota={10} onExport={startExport} /><ExportMenu :used="10" :quota="10" @export="startExport" />Formats and period
Name the period the quota resets on, and list the formats the backend produces:
<ExportMenu
used={1}
quota={5}
period="this week"
formats={[
{ value: 'csv', label: 'Download CSV' },
{ value: 'xlsx', label: 'Download Excel' },
{ value: 'pdf', label: 'Download PDF' },
]}
onExport={startExport}
/><ExportMenu
:used="1"
:quota="5"
period="this week"
:formats="[
{ value: 'csv', label: 'Download CSV' },
{ value: 'xlsx', label: 'Download Excel' },
{ value: 'pdf', label: 'Download PDF' },
]"
@export="startExport"
/>