@sewadah/selaras-docs 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.2.4
4
+
5
+ [compare changes](https://github.com/sewadahapp/selaras/compare/selaras-docs-v1.2.3...selaras-docs-v1.2.4)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - **feat(docs):** add interactive application blocks ([102ee68](https://github.com/sewadahapp/selaras/commit/102ee68bef82d12b07dc4788dc8ea2378435e0ea))
10
+ - **feat(docs):** add error pages and local recovery boundaries ([371ed7a](https://github.com/sewadahapp/selaras/commit/371ed7a4fd873fa7e4647ab0b89b85dc0bb7a4b7))
11
+ - **feat(docs):** add SEO metadata and static sharing images ([f6ec141](https://github.com/sewadahapp/selaras/commit/f6ec14152def06f91a62292b53ae2df50813b1d5))
12
+ - **feat(docs):** add Seirama branding and design kit ([9fd9f19](https://github.com/sewadahapp/selaras/commit/9fd9f1948feaa19897ae24e982d33099d50a732c))
13
+ - **feat(forms):** add leading and trailing content slots ([ebd0948](https://github.com/sewadahapp/selaras/commit/ebd094813a274d444319cb0354387c770ab8a6ef))
14
+
15
+ ### 🩹 Fixes
16
+
17
+ - **fix(docs):** configure custom domain deployment ([b65908a](https://github.com/sewadahapp/selaras/commit/b65908a7aa4ad0d0b67029b41c338633e9faa50d))
18
+
19
+ Published against `@sewadah/selaras` ^0.9.0.
20
+
3
21
  ## v1.2.3
4
22
 
5
23
  [compare changes](https://github.com/sewadahapp/selaras/compare/selaras-docs-v1.2.2...selaras-docs-v1.2.3)
package/README.md CHANGED
@@ -119,6 +119,65 @@ pieces can be replaced on their own:
119
119
  For example, a `components/DocsFooterBrand.vue` containing `<DocsLogo />` adds
120
120
  the logo to the footer while leaving the header as configured.
121
121
 
122
+ ## Error recovery
123
+
124
+ The layer provides a default `error.vue` for missing pages and fatal errors.
125
+ It preserves the HTTP status code, keeps error pages out of search indexes and
126
+ offers a home link or reload action. The standalone screen avoids loading
127
+ navigation and Content data so it can render when those services fail.
128
+
129
+ Client-side page failures show a local recovery state inside the layout, keeping
130
+ the header and sidebar available. `DocsExample` isolates each live example;
131
+ retrying remounts its content. Navigating to another route clears the page
132
+ boundary. Server rendering errors still use the standalone error screen.
133
+
134
+ Content query failures show a retry state with HTTP 500 during server rendering.
135
+ Only a successful query with no matching document is treated as a 404.
136
+ If a request cannot recover in place, retry reloads the current page to start a
137
+ fresh session. Reloads happen only after the user chooses to retry.
138
+
139
+ Customize the messages and home destination in `app.config.ts`:
140
+
141
+ ```ts
142
+ export default defineAppConfig({
143
+ selarasDocs: {
144
+ errors: {
145
+ homeTo: '/guide/introduction',
146
+ homeLabel: 'Back to the guide',
147
+ retryLabel: 'Try again',
148
+ reloadLabel: 'Reload page',
149
+ retryingLabel: 'Trying again…',
150
+ notFound: { title: 'Page not found', description: 'Start again from the guide.' },
151
+ fatal: { title: 'Something went wrong' },
152
+ page: { title: 'This page couldn’t load' },
153
+ example: { title: 'This example couldn’t load' },
154
+ },
155
+ },
156
+ })
157
+ ```
158
+
159
+ Home destinations are internal routes resolved under the app's `baseURL`.
160
+ The default destination is `/`. Provide an existing page if your site has no
161
+ homepage. Public recovery copy never includes raw exceptions or stack traces.
162
+
163
+ Override `components/DocsErrorIllustration.vue` to replace the default geometric
164
+ document illustration. The optional `missing` boolean distinguishes a 404 from
165
+ other errors. Use inherited theme tokens in inline SVGs for light/dark support.
166
+ `DocsErrorState` also exposes an `illustration` slot. Override `DocsErrorPage`
167
+ for the standalone screen's composition, or supply your own `error.vue` for
168
+ application-specific providers. Applications with their own `app.vue` should
169
+ retain the page boundary inside their layout:
170
+
171
+ ```vue
172
+ <template>
173
+ <NuxtLayout>
174
+ <DocsErrorBoundary>
175
+ <NuxtPage />
176
+ </DocsErrorBoundary>
177
+ </NuxtLayout>
178
+ </template>
179
+ ```
180
+
122
181
  For a host that owns Tailwind compilation or uses a class prefix, set
123
182
  `selarasDocs: { css: false }` in `nuxt.config.ts` and import Selaras and the
124
183
  docs stylesheet from the host CSS entry.
package/app/app.config.ts CHANGED
@@ -31,7 +31,7 @@ export default defineAppConfig({
31
31
  links: [
32
32
  {
33
33
  label: 'Powered by Selaras',
34
- to: 'https://sewadahapp.github.io/selaras',
34
+ to: 'https://selarasui.my.id',
35
35
  target: '_blank',
36
36
  rel: 'noreferrer',
37
37
  },
package/app/app.vue CHANGED
@@ -13,7 +13,9 @@ useHead(() => {
13
13
  <template>
14
14
  <SApp>
15
15
  <NuxtLayout>
16
- <NuxtPage />
16
+ <DocsErrorBoundary>
17
+ <NuxtPage />
18
+ </DocsErrorBoundary>
17
19
  </NuxtLayout>
18
20
  <ClientOnly>
19
21
  <DocsSearch />
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { SelarasDocsErrorKind } from '../../types'
3
+ import { useTemplateRef, watch } from 'vue'
4
+ import { useRouter } from '#app'
5
+
6
+ const props = withDefaults(defineProps<{
7
+ kind?: Extract<SelarasDocsErrorKind, 'page' | 'example'>
8
+ resetKey?: string
9
+ }>(), { kind: 'page' })
10
+
11
+ const boundary = useTemplateRef('boundary')
12
+ const router = useRouter()
13
+ watch([() => router.currentRoute.value.fullPath, () => props.resetKey], () => boundary.value?.clearError())
14
+
15
+ function isMissing(error: Error) {
16
+ return 'statusCode' in error && error.statusCode === 404
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <NuxtErrorBoundary ref="boundary">
22
+ <slot />
23
+ <template #error="{ error, clearError }">
24
+ <DocsErrorState :kind="kind === 'page' && isMissing(error) ? 'notFound' : kind" @retry="clearError" />
25
+ </template>
26
+ </NuxtErrorBoundary>
27
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ missing?: boolean }>()
3
+ </script>
4
+
5
+ <template>
6
+ <svg viewBox="0 0 400 240" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
7
+ <path d="M40 187H360" stroke="var(--selaras-resolved-border-default)" stroke-width="2" stroke-linecap="round" />
8
+ <rect x="113" y="41" width="148" height="159" rx="12" fill="var(--selaras-resolved-surface-elevated)" stroke="var(--selaras-resolved-border-default)" stroke-width="2" />
9
+ <path d="M144 24H231L277 70V170C277 177 272 182 265 182H144C137 182 132 177 132 170V36C132 29 137 24 144 24Z" fill="var(--selaras-resolved-surface-default)" stroke="var(--selaras-resolved-color-primary-fill)" stroke-width="2.5" stroke-linejoin="round" />
10
+ <path d="M231 25V58C231 65 236 70 243 70H276" stroke="var(--selaras-resolved-color-primary-fill)" stroke-width="2.5" stroke-linejoin="round" />
11
+ <path d="M154 61H193M154 84H213" stroke="var(--selaras-resolved-border-default)" stroke-width="5" stroke-linecap="round" />
12
+ <rect x="154" y="108" width="57" height="48" rx="6" fill="var(--selaras-resolved-color-primary-subtle)" />
13
+ <path v-if="missing" d="M225 113H254V154H225V113Z" stroke="var(--selaras-resolved-color-primary-fill)" stroke-width="2" stroke-dasharray="4 5" stroke-linejoin="round" />
14
+ <path v-else d="M233 116V135M233 146V147" stroke="var(--selaras-resolved-color-primary-fill)" stroke-width="4" stroke-linecap="round" />
15
+ <path d="M56 153C56 122 84 119 105 132M290 109C318 103 342 122 333 146" stroke="var(--selaras-resolved-border-default)" stroke-width="2" stroke-dasharray="4 7" stroke-linecap="round" />
16
+ <circle cx="55" cy="159" r="6" fill="var(--selaras-resolved-color-primary-fill)" />
17
+ <rect x="311" y="144" width="44" height="37" rx="6" fill="var(--selaras-resolved-color-primary-subtle)" stroke="var(--selaras-resolved-color-primary-fill)" stroke-width="2" />
18
+ <path d="M323 163H343M333 153V173" stroke="var(--selaras-resolved-color-primary-fill)" stroke-width="2" stroke-linecap="round" />
19
+ </svg>
20
+ </template>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import type { NuxtError } from '#app'
3
+
4
+ const props = defineProps<{ error: NuxtError }>()
5
+ const config = useRuntimeConfig()
6
+ const appConfig = useAppConfig()
7
+ const siteName = computed(() => appConfig.selarasDocs?.site?.name ?? 'Documentation')
8
+ const missing = computed(() => props.error.statusCode === 404)
9
+ const pending = ref(false)
10
+
11
+ useSeoMeta({
12
+ title: () => `${missing.value ? 'Page not found' : 'Something went wrong'} — ${siteName.value}`,
13
+ robots: 'noindex',
14
+ })
15
+
16
+ function reload() {
17
+ pending.value = true
18
+ window.location.reload()
19
+ }
20
+
21
+ async function goHome(event: MouseEvent) {
22
+ event.preventDefault()
23
+ const path = appConfig.selarasDocs?.errors?.homeTo ?? '/'
24
+ try {
25
+ await clearError({ redirect: path })
26
+ }
27
+ catch {
28
+ window.location.assign(`${config.app.baseURL.replace(/\/$/, '')}/${path.replace(/^\//, '')}`)
29
+ }
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div class="selaras-docs-root selaras-docs-error-page">
35
+ <header class="selaras-docs-error-header">
36
+ <a :href="config.app.baseURL" class="selaras-docs-error-brand" :aria-label="`${siteName} home`">
37
+ <DocsLogo />
38
+ <span>{{ siteName }}</span>
39
+ </a>
40
+ </header>
41
+ <main class="selaras-docs-error-main">
42
+ <DocsErrorState :kind="missing ? 'notFound' : 'fatal'" :pending="pending" @retry="reload" @home="goHome" />
43
+ </main>
44
+ </div>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .selaras-docs-error-page {
49
+ min-height: 100svh;
50
+ color: var(--selaras-resolved-text-default);
51
+ }
52
+
53
+ .selaras-docs-error-header {
54
+ width: min(100%, 90rem);
55
+ margin: 0 auto;
56
+ padding: 1.5rem;
57
+ }
58
+
59
+ .selaras-docs-error-brand {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ gap: 0.75rem;
63
+ color: inherit;
64
+ font-size: 1.125rem;
65
+ font-weight: 650;
66
+ text-decoration: none;
67
+ }
68
+
69
+ .selaras-docs-error-brand:focus-visible {
70
+ border-radius: var(--selaras-resolved-radius-sm);
71
+ outline: 2px solid var(--selaras-resolved-color-primary-focus);
72
+ outline-offset: 4px;
73
+ }
74
+
75
+ .selaras-docs-error-main {
76
+ display: grid;
77
+ align-items: center;
78
+ min-height: calc(100svh - 6rem);
79
+ padding: 1.5rem 1.5rem 5rem;
80
+ }
81
+ </style>
@@ -0,0 +1,161 @@
1
+ <script setup lang="ts">
2
+ import type { SelarasDocsErrorKind } from '../../types'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ kind?: SelarasDocsErrorKind
6
+ pending?: boolean
7
+ showHome?: boolean
8
+ }>(), { kind: 'page', pending: false, showHome: undefined })
9
+
10
+ const emit = defineEmits<{ retry: [], home: [event: MouseEvent] }>()
11
+ const appConfig = useAppConfig()
12
+ const config = useRuntimeConfig()
13
+ const homeHref = computed(() => {
14
+ const path = appConfig.selarasDocs?.errors?.homeTo ?? '/'
15
+ const base = config.app.baseURL.replace(/\/$/, '')
16
+ return `${base}/${path.replace(/^\//, '')}`
17
+ })
18
+ const headingId = useId()
19
+ const heading = useTemplateRef<HTMLElement>('heading')
20
+ onMounted(() => {
21
+ if (props.kind !== 'example')
22
+ heading.value?.focus({ preventScroll: true })
23
+ })
24
+ const copy = computed(() => {
25
+ const defaults = {
26
+ notFound: { title: 'Page not found', description: 'The link may have changed, or this page may no longer exist. You can start again from the homepage.' },
27
+ fatal: { title: 'Something went wrong', description: 'We couldn’t load this page. Reload it to try again, or return to the homepage.' },
28
+ page: { title: 'This page couldn’t load', description: 'Please try again. You can still use the navigation to explore other pages.' },
29
+ example: { title: 'This example couldn’t load', description: 'Try loading the example again. The rest of the documentation is still available.' },
30
+ }
31
+ return { ...defaults[props.kind], ...appConfig.selarasDocs?.errors?.[props.kind] }
32
+ })
33
+ const labels = computed(() => appConfig.selarasDocs?.errors ?? {})
34
+ const showHome = computed(() => props.showHome ?? props.kind !== 'example')
35
+ </script>
36
+
37
+ <template>
38
+ <section
39
+ class="selaras-docs-error-state not-prose"
40
+ :data-error-kind="kind"
41
+ :aria-labelledby="headingId"
42
+ :role="kind === 'example' ? 'alert' : undefined"
43
+ >
44
+ <div class="selaras-docs-error-artwork">
45
+ <slot name="illustration">
46
+ <DocsErrorIllustration :missing="kind === 'notFound'" />
47
+ </slot>
48
+ </div>
49
+ <div class="selaras-docs-error-copy">
50
+ <component :is="kind === 'example' ? 'h3' : 'h1'" :id="headingId" ref="heading" class="selaras-docs-error-title" :tabindex="kind === 'example' ? undefined : -1">
51
+ {{ copy.title }}
52
+ </component>
53
+ <p class="selaras-docs-error-description">
54
+ {{ copy.description }}
55
+ </p>
56
+ <div class="selaras-docs-error-actions">
57
+ <SButton v-if="kind !== 'notFound'" :loading="pending" :disabled="pending" @click="emit('retry')">
58
+ {{ pending ? labels.retryingLabel ?? 'Trying again…' : kind === 'fatal' ? labels.reloadLabel ?? 'Reload page' : labels.retryLabel ?? 'Try again' }}
59
+ </SButton>
60
+ <SButton v-if="showHome" as="a" :href="homeHref" :variant="kind === 'notFound' ? 'solid' : 'outline'" :color="kind === 'notFound' ? 'primary' : 'neutral'" @click="emit('home', $event)">
61
+ {{ labels.homeLabel ?? 'Go to homepage' }}
62
+ </SButton>
63
+ </div>
64
+ </div>
65
+ </section>
66
+ </template>
67
+
68
+ <style scoped>
69
+ .selaras-docs-error-state {
70
+ width: min(100%, 42rem);
71
+ margin: 3rem auto;
72
+ color: var(--selaras-resolved-text-default);
73
+ text-align: center;
74
+ }
75
+
76
+ .selaras-docs-error-artwork {
77
+ width: min(100%, 22rem);
78
+ margin: 0 auto 1.5rem;
79
+ }
80
+
81
+ .selaras-docs-error-artwork :deep(svg) {
82
+ display: block;
83
+ width: 100%;
84
+ height: auto;
85
+ }
86
+
87
+ .selaras-docs-error-title {
88
+ margin: 0;
89
+ font-size: clamp(1.75rem, 1.4rem + 1vw, 2.5rem);
90
+ font-weight: 650;
91
+ line-height: 1.15;
92
+ letter-spacing: -0.025em;
93
+ text-wrap: balance;
94
+ }
95
+
96
+ .selaras-docs-error-title:focus {
97
+ outline: none;
98
+ }
99
+
100
+ .selaras-docs-error-description {
101
+ max-width: 44ch;
102
+ margin: 1rem auto 0;
103
+ color: var(--selaras-resolved-text-muted);
104
+ font-size: 1rem;
105
+ line-height: 1.7;
106
+ }
107
+
108
+ .selaras-docs-error-actions {
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ justify-content: center;
112
+ gap: 0.75rem;
113
+ margin-top: 1.75rem;
114
+ }
115
+
116
+ .selaras-docs-error-state[data-error-kind="example"] {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 1.25rem;
120
+ width: 100%;
121
+ margin: 0;
122
+ padding: 1.5rem;
123
+ border: 1px solid var(--selaras-resolved-border-default);
124
+ border-radius: var(--selaras-resolved-radius-lg);
125
+ background: var(--selaras-resolved-surface-elevated);
126
+ text-align: start;
127
+ }
128
+
129
+ [data-error-kind="example"] .selaras-docs-error-artwork {
130
+ flex: none;
131
+ width: 6rem;
132
+ margin: 0;
133
+ }
134
+
135
+ [data-error-kind="example"] .selaras-docs-error-title {
136
+ font-size: 1rem;
137
+ line-height: 1.4;
138
+ letter-spacing: normal;
139
+ }
140
+
141
+ [data-error-kind="example"] .selaras-docs-error-description {
142
+ margin: 0.375rem 0 0;
143
+ font-size: 0.875rem;
144
+ }
145
+
146
+ [data-error-kind="example"] .selaras-docs-error-actions {
147
+ justify-content: flex-start;
148
+ margin-top: 1rem;
149
+ }
150
+
151
+ @media (max-width: 30rem) {
152
+ .selaras-docs-error-state[data-error-kind="example"] {
153
+ align-items: flex-start;
154
+ flex-direction: column;
155
+ }
156
+
157
+ [data-error-kind="example"] .selaras-docs-error-artwork {
158
+ display: none;
159
+ }
160
+ }
161
+ </style>
@@ -18,7 +18,9 @@ const resolvedComponent = computed(() => {
18
18
 
19
19
  <template>
20
20
  <section v-if="resolvedComponent" :data-example="name">
21
- <component :is="resolvedComponent" />
21
+ <DocsErrorBoundary kind="example" :reset-key="name">
22
+ <component :is="resolvedComponent" />
23
+ </DocsErrorBoundary>
22
24
  </section>
23
25
  <p v-else role="alert">
24
26
  Example "{{ name }}" not found
package/app/error.vue ADDED
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import type { NuxtError } from '#app'
3
+
4
+ defineProps<{ error: NuxtError }>()
5
+ </script>
6
+
7
+ <template>
8
+ <SApp>
9
+ <DocsErrorPage :error="error" />
10
+ </SApp>
11
+ </template>
@@ -10,14 +10,30 @@ import ProsePre from '@sewadah/selaras/components/ProsePre.vue'
10
10
 
11
11
  const route = useRoute()
12
12
  const appConfig = useAppConfig()
13
- const { data: page } = await useAsyncData(`selaras-docs:${route.path}`, () => queryCollection('docs').path(route.path).first())
13
+ const { data: page, error, status, refresh } = await useAsyncData(`selaras-docs:${route.path}`, () => queryCollection('docs').path(route.path).first())
14
14
 
15
- if (!page.value) {
15
+ if (!page.value && !error.value) {
16
16
  throw createError({ statusCode: 404, statusMessage: 'Documentation page not found', fatal: true })
17
17
  }
18
+ if (import.meta.server && error.value)
19
+ setResponseStatus(500)
20
+
21
+ async function retryPage() {
22
+ await refresh()
23
+ if (import.meta.client && error.value) {
24
+ window.location.reload()
25
+ return
26
+ }
27
+ if (!page.value && !error.value)
28
+ showError({ statusCode: 404, statusMessage: 'Documentation page not found' })
29
+ }
18
30
 
19
31
  definePageMeta({ layout: 'selaras-docs' })
20
- useSeoMeta({ title: page.value.title, description: page.value.description ?? appConfig.selarasDocs?.site?.description })
32
+ useSeoMeta({
33
+ title: () => page.value?.title ?? appConfig.selarasDocs?.errors?.page?.title ?? 'Documentation unavailable',
34
+ description: () => page.value?.description ?? appConfig.selarasDocs?.site?.description,
35
+ robots: () => error.value ? 'noindex' : undefined,
36
+ })
21
37
  useHead(() => ({
22
38
  ...(appConfig.selarasDocs?.site?.titleTemplate && { titleTemplate: appConfig.selarasDocs.site.titleTemplate }),
23
39
  ...(appConfig.selarasDocs?.site?.url && {
@@ -40,7 +56,8 @@ const surround = useDocsSurround()
40
56
  </script>
41
57
 
42
58
  <template>
43
- <SContainer size="full" class="selaras-docs-page" :class="{ 'selaras-docs-page--fluid': appConfig.selarasDocs?.main?.fluid }">
59
+ <DocsErrorState v-if="error || !page" kind="page" :pending="status === 'pending'" @retry="retryPage" />
60
+ <SContainer v-else size="full" class="selaras-docs-page" :class="{ 'selaras-docs-page--fluid': appConfig.selarasDocs?.main?.fluid }">
44
61
  <div class="selaras-docs-page-grid">
45
62
  <article class="selaras-docs-article">
46
63
  <p v-if="page!.description" class="selaras-docs-description">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sewadah/selaras-docs",
3
3
  "type": "module",
4
- "version": "1.2.3",
4
+ "version": "1.2.4",
5
5
  "description": "Nuxt Content documentation layer for Selaras",
6
6
  "repository": {
7
7
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@nuxt/content": "^3.16.0",
35
35
  "@nuxt/kit": "^4.5.2",
36
- "@sewadah/selaras": "^0.8.0",
36
+ "@sewadah/selaras": "^0.9.0",
37
37
  "tailwindcss": "^4.3.0"
38
38
  }
39
39
  }
package/types.d.ts CHANGED
@@ -37,7 +37,23 @@ export interface SelarasDocsLogo {
37
37
  tint?: boolean
38
38
  }
39
39
 
40
+ export type SelarasDocsErrorKind = 'notFound' | 'fatal' | 'page' | 'example'
41
+
42
+ export interface SelarasDocsErrorCopy {
43
+ title?: string
44
+ description?: string
45
+ }
46
+
40
47
  export interface SelarasDocsAppConfig {
48
+ /** Recovery messages and the destination of error-screen home links. */
49
+ errors?: Partial<Record<SelarasDocsErrorKind, SelarasDocsErrorCopy>> & {
50
+ /** An internal route, resolved under the application's base URL. @default '/' */
51
+ homeTo?: string
52
+ homeLabel?: string
53
+ retryLabel?: string
54
+ reloadLabel?: string
55
+ retryingLabel?: string
56
+ }
41
57
  site?: {
42
58
  name?: string
43
59
  description?: string