Gavia UIv1.0.0

Recipes

Working compositions with validation, cancellation, retry and application-owned operations.

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

Documents, agreements and research.
12 materials
Material filters
MaterialStatusActions
Access and rolesIn progress
Access and roles · 12In progress
Access and roles · 8In progress
Research planIn 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>
Source and guide