tauri-plugin-ota-self-update 0.2.1

Self-hosted OTA updates for Tauri v2 web assets.
Documentation
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import CodeBlock from '../components/docs/CodeBlock.vue'
import { normalizeLocale } from '../content/locales'
import { getSiteContent } from '../content/loadSiteContent'

const route = useRoute()
const locale = computed(() => normalizeLocale(String(route.params.locale || 'en')))
const content = computed(() => getSiteContent(locale.value))

const allPages = computed(() =>
  content.value.docs.groups.flatMap((g) =>
    g.pages.map((p) => ({ ...p, groupId: g.id, groupTitle: g.title }))
  )
)
const currentId = computed(() => String(route.params.slug || 'installation'))
const current = computed(() => allPages.value.find((p) => p.id === currentId.value) || allPages.value[0])

const docsLink = (id: string) => (locale.value === 'ru' ? `/ru/docs/${id}` : `/docs/${id}`)
const docsRoot = computed(() => (locale.value === 'ru' ? '/ru/docs' : '/docs'))
const sectionAnchor = (heading: string) =>
  heading
    .toLowerCase()
    .replace(/[^a-z0-9а-яё]+/gi, '-')
    .replace(/^-+|-+$/g, '')

const toc = computed(() =>
  (current.value?.sections || []).map((section) => ({
    id: sectionAnchor(section.heading),
    heading: section.heading
  }))
)

const activeHeading = ref('')
let observer: IntersectionObserver | null = null

function bindAnchors() {
  observer?.disconnect()
  nextTick(() => {
    document.querySelectorAll<HTMLElement>('[data-doc-section-anchor="true"]').forEach((el) => observer?.observe(el))
  })
}

onMounted(() => {
  observer = new IntersectionObserver(
    (entries) => {
      const visible = entries.filter((entry) => entry.isIntersecting)
      if (!visible.length) return
      const top = visible.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]
      activeHeading.value = top.target.id
    },
    { rootMargin: '-20% 0px -65% 0px', threshold: [0, 1] }
  )
  bindAnchors()
})

watch(
  () => current.value?.id,
  () => {
    activeHeading.value = ''
    bindAnchors()
  }
)

onBeforeUnmount(() => {
  observer?.disconnect()
  observer = null
})
</script>

<template>
  <section class="docs-shell">
    <aside class="card h-fit space-y-4 lg:sticky lg:top-4">
      <div>
        <h2 class="text-lg font-semibold">{{ content.docs.docsTitle }}</h2>
        <p class="mt-2 text-xs text-[#9db1ff]">{{ content.docs.docsIntro }}</p>
      </div>
      <nav class="space-y-4">
        <details v-for="group in content.docs.groups" :key="group.id" class="sidebar-group space-y-2" open>
          <summary class="text-xs uppercase tracking-[0.18em] text-[#8ea2ed]">{{ group.title }}</summary>
          <div class="mt-2 space-y-1">
            <RouterLink
              v-for="page in group.pages"
              :key="page.id"
              :to="docsLink(page.id)"
              @click="window.scrollTo({ top: 0, behavior: 'auto' })"
              class="block rounded px-2 py-1 text-sm hover:bg-[#1a2753]"
              :class="page.id === current.id ? 'bg-[#1a2753] text-white' : 'text-[#dce3ff]'"
            >
              {{ page.title }}
            </RouterLink>
          </div>
        </details>
      </nav>
    </aside>

    <article class="card docs-prose space-y-6">
      <div class="text-xs text-[#9db1ff]">
        <RouterLink :to="docsRoot" class="hover:underline">{{ content.docs.docsTitle }}</RouterLink>
        <span> / {{ current.groupTitle }} / {{ current.title }}</span>
      </div>

      <header class="space-y-2">
        <h1 class="text-2xl font-semibold">{{ current.title }}</h1>
        <p class="text-sm leading-7 text-[#c8d3ff]">{{ current.summary }}</p>
      </header>

      <section
        v-for="section in current.sections"
        :id="sectionAnchor(section.heading)"
        :key="section.heading"
        data-doc-section-anchor="true"
        class="space-y-3 border-t border-[#26315f] pt-4"
      >
        <h2 class="text-lg font-semibold">{{ section.heading }}</h2>
        <p v-for="paragraph in section.body" :key="paragraph" class="text-sm leading-7 text-[#dce3ff]">{{ paragraph }}</p>
        <ul v-if="section.bullets?.length" class="list-disc space-y-1 pl-5 text-sm text-[#dce3ff]">
          <li v-for="bullet in section.bullets" :key="bullet">{{ bullet }}</li>
        </ul>
        <div v-if="section.checklist?.length" class="docs-checklist">
          <label v-for="item in section.checklist" :key="item" class="docs-checklist-item">
            <input type="checkbox" checked disabled />
            <span>{{ item }}</span>
          </label>
        </div>
        <div v-if="section.table" class="docs-table-wrap">
          <table class="docs-table">
            <thead>
              <tr>
                <th v-for="header in section.table.headers" :key="header">{{ header }}</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(row, idx) in section.table.rows" :key="idx">
                <td v-for="(col, colIdx) in row" :key="`${idx}-${colIdx}`">{{ col }}</td>
              </tr>
            </tbody>
          </table>
        </div>
        <CodeBlock
          v-if="section.code"
          :title="section.code.title"
          :language="section.code.language"
          :value="section.code.value"
        />
        <p
          v-if="section.note"
          class="docs-callout"
          :class="`docs-callout-${section.noteVariant || 'info'}`"
        >
          {{ section.note }}
        </p>
        <div v-if="section.faq?.length" class="space-y-2">
          <details v-for="item in section.faq" :key="item.q" class="docs-faq">
            <summary>{{ item.q }}</summary>
            <p>{{ item.a }}</p>
          </details>
        </div>
      </section>
    </article>

    <aside class="card hidden h-fit lg:block lg:sticky lg:top-4">
      <h3 class="mb-3 text-sm font-semibold">On this page</h3>
      <nav class="space-y-1">
        <a
          v-for="item in toc"
          :key="item.id"
          :href="`#${item.id}`"
          class="toc-link"
          :class="{ 'toc-link-active': activeHeading === item.id }"
        >
          {{ item.heading }}
        </a>
      </nav>
    </aside>
  </section>
</template>