Recipes
Keep operations in the consumer
Recipes compose public components. Replace simulated application operations with your API and preserve loading, failure, cancel and retry paths. Each selected recipe loads its own live component and copyable SFC.
Format binary file sizes
formatWlBytes keeps 1024-based units and Intl formatting for existing two-argument calls. The optional third argument supplies unit labels, a decimal separator, grouping, precision and a maximum unit. FileUpload uses it while preserving its 0.x locale overrides, rounding and megabyte cap.
Show Vue code
import { formatWlBytes } from "gavia-ui/utils";
formatWlBytes("en", 1536);
formatWlBytes("en", 1536, { unitLabels: { kilobyte: "KiB" } });Search, filters, sorting and pagination. Create and edit in Drawer, confirm deletion and show Toast notifications.
Team materials
| Material | Status | Actions |
|---|---|---|
| Access and roles | In progress | |
| Access and roles · 12 | In progress | |
| Access and roles · 8 | In progress | |
| Research plan | In progress |
Found: 12 · page 1 of 3
Show Vue code
<script setup lang="ts">
import { useWlConfirm, useWlToast, WlButton, WlDrawer, WlEmpty, WlField, WlFilterBar, WlInput, WlPageHeader, WlPagination, WlPill, WlSelect, WlTable, type WlTableColumn, WlTag } from "gavia-ui";
import { computed, nextTick, ref, watch } from "vue";
type Material = { id: number; title: string; status: string };
const materials = ref<Material[]>(Array.from({ length: 12 }, (_, index) => ({
id: index + 1, title: ["Team guide", "Research plan", "Review guidelines", "Access and roles"][index % 4] + (index < 4 ? "" : ` · ${index + 1}`),
status: index % 2 ? "progress" : "done"
})));
const search = ref("");
const status = ref<string | null>(null);
const order = ref<string | null>("asc");
const page = ref(1);
const confirm = useWlConfirm();
const toast = useWlToast();
const filtered = computed(() => materials.value.filter((item) =>
item.title.toLocaleLowerCase("ru").includes(search.value.toLocaleLowerCase("ru")) && (!status.value || item.status === status.value))
.sort((a, b) => a.title.localeCompare(b.title, "ru") * (order.value === "desc" ? -1 : 1)));
const pageCount = computed(() => Math.max(1, Math.ceil(filtered.value.length / 4)));
const visibleRows = computed(() => filtered.value.slice((page.value - 1) * 4, page.value * 4));
watch([search, status, order], () => {
page.value = 1;
});
watch(pageCount, (count) => {
page.value = Math.min(page.value, count);
});
const columns: readonly WlTableColumn<Material>[] = [{ key: "title", label: "Material" }, { key: "status", label: "Status" }, { key: "actions", label: "Actions", kind: "virtual" }];
function clear(): void {
search.value = "";
status.value = null;
}
const editing = ref(false);
const editingId = ref<number | null>(null);
const title = ref("");
const titleError = ref("");
let nextId = 13;
function edit(item?: Material): void {
editingId.value = item?.id ?? null;
title.value = item?.title ?? "";
titleError.value = "";
editing.value = true;
}
async function save(): Promise<void> {
titleError.value = title.value.trim() ? "" : "Enter a title.";
if (titleError.value) {
await nextTick();
document.getElementById("recipe-material-title")?.focus();
return;
}
const current = materials.value.find((item) => item.id === editingId.value);
if (current) current.title = title.value.trim();
else materials.value.push({ id: nextId++, title: title.value.trim(), status: "progress" });
editing.value = false;
toast.ok("Material saved");
}
function remove(id: number): void {
const item = materials.value.find((entry) => entry.id === id);
if (!item) return;
confirm.confirmDanger({
header: "Delete material?", message: `«${item.title}${"» will be deleted."}`, acceptLabel: "Delete", rejectLabel: "Cancel",
accept: () => {
materials.value = materials.value.filter((entry) => entry.id !== id);
toast.ok("Material deleted");
}
});
}
</script>
<template>
<section class="wl-stack" data-space="lg" aria-label="Material list">
<WlPageHeader
title="Team materials"
description="Documents, agreements and research."
:heading-level="2"
size="md"
>
<template #meta><WlTag>{{ materials.length }} {{ "materials" }}</WlTag></template>
<template #actions><WlButton variant="primary" @click="edit()">{{ "Create material" }}</WlButton></template>
</WlPageHeader>
<WlInput
v-model="search"
type="search"
placeholder="Find a material"
aria-label="Find a material"
/>
<WlFilterBar
:active-count="Number(Boolean(status))"
aria-label="Material filters"
panel-title="Material filters"
@clear="clear"
>
<WlSelect
v-model="status"
:options="[{ label: 'Done', value: 'done' }, { label: 'In progress', value: 'progress' }]"
option-label="label"
option-value="value"
aria-label="Material status"
placeholder="All statuses"
/>
<WlSelect
v-model="order"
:options="[{ label: 'Name A–Z', value: 'asc' }, { label: 'Name Z–A', value: 'desc' }]"
option-label="label"
option-value="value"
aria-label="Material order"
/>
<template #summary><WlTag
v-if="status"
removable
remove-label="Remove status filter"
@remove="status = null"
>{{ status === 'done' ? "Done" : "In progress" }}</WlTag></template>
</WlFilterBar>
<div
v-if="filtered.length"
class="ds-material-table"
role="region"
aria-label="Team materials"
tabindex="0"
><WlTable :columns="columns" :value="visibleRows" :pt="{ root: { style: { minWidth: '640px' } } }">
<template #cell-status="{ value }">
<WlPill
:tone="value === 'done' ? 'success' : 'info'"
:label="value === 'done' ? 'Done' : 'In progress'"
/>
</template>
<template #cell-actions="{ row }"><div class="wl-inline" data-space="xs"><WlButton
size="sm"
variant="ghost"
:aria-label="`${'Edit '}${row.title}`"
@click="edit(materials.find((item) => item.id === row.id))"
>{{ "Edit" }}</WlButton><WlButton
size="sm"
variant="ghost"
tone="danger"
:aria-label="`${'Delete '}${row.title}`"
@click="remove(Number(row.id))"
>{{ "Delete" }}</WlButton></div></template>
</WlTable></div>
<WlEmpty
v-else
icon="search"
title="No materials found"
description="Change the query or clear the filters."
><template #action><WlButton @click="clear">{{ "Clear search" }}</WlButton></template></WlEmpty>
<div class="wl-inline" data-space="md">
<WlPagination v-if="filtered.length" v-model:page="page" :page-count="pageCount" />
<p class="wl-text-small wl-text-muted" role="status">
{{ "Found:" }} {{ filtered.length }} {{ "· page" }}
{{ page }} {{ "of" }} {{ pageCount }}
</p>
</div>
<WlDrawer v-model:visible="editing" :header="editingId ? 'Edit material' : 'New material'">
<form
id="recipe-material-form"
class="wl-stack"
data-space="lg"
@submit.prevent="save"
>
<WlField
id="recipe-material-title"
v-slot="field"
label="Title"
required
:error="titleError"
><WlInput
:id="field.id"
v-model="title"
:invalid="field.invalid"
:aria-describedby="field.ariaDescribedby"
maxlength="120"
/></WlField>
<p class="wl-text-small wl-text-muted">{{ "To apply changes, click Save." }}</p>
</form>
<template #footer>
<WlButton variant="ghost" @click="editing = false">{{ "Cancel" }}</WlButton>
<WlButton variant="primary" type="submit" form="recipe-material-form">{{ "Save" }}</WlButton>
</template>
</WlDrawer>
</section>
</template>
<style scoped>
.ds-material-table { overflow: auto; max-width: 100%; }
</style>Examples use sample data. Saving and loading are simulated in the browser. Connect server requests in your application. The workflow and copyable code share one Vue file.
Styles, theme and services setup
// main.ts — Import CSS explicitly; set the theme using data-wl-theme on html.
import { createApp } from "vue";
import { WlConfig, WlToastService, WlConfirmationService, createWlPt, wlLocaleEn } from "gavia-ui";
import "gavia-ui/styles/reset.css";
import "gavia-ui/styles/base.css";
import "gavia-ui/styles/primitives.css";
import "gavia-ui/themes/white.css";
import "gavia-ui/themes/graphite.css";
import "gavia-ui/themes/newspaper.css";
import "gavia-ui/styles/fonts/gavia.css";
import "gavia-ui/themes/gavia.css";
import "gavia-ui/themes/gavia-dark.css";
import App from "./App.vue";
document.documentElement.dataset.wlTheme = "white";
// WlConfig merges application pt with defaults; local component pt is applied last.
const pt = createWlPt({ button: { root: { "data-ui": "action" } } });
createApp(App).use(WlConfig, { locale: wlLocaleEn, motion: true, pt })
.use(WlToastService).use(WlConfirmationService).mount("#app");
// App.vue: import WlToast/WlConfirmDialog and mount one of each
// container alongside the page. Notifications and confirmations use them.
// <template><YourPage /><WlToast /><WlConfirmDialog /></template>