@uxfront/layer-docs 0.1.1 → 0.2.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/app/components/app/AppHeaderAttribution.vue +10 -1
  3. package/app/components/content/FrameworkSwitcher.vue +1 -0
  4. package/app/components/content/GradientPageHero.vue +35 -0
  5. package/app/components/docs/DocsAsideLeftTop.vue +24 -0
  6. package/app/components/docs/DocsFrameworkSelect.vue +18 -0
  7. package/app/composables/useFramework.ts +20 -29
  8. package/app/layouts/default.vue +1 -0
  9. package/app/plugins/posthog.client.ts +24 -5
  10. package/i18n/locales/ar.json +24 -0
  11. package/i18n/locales/be.json +24 -0
  12. package/i18n/locales/bn.json +24 -0
  13. package/i18n/locales/ca.json +24 -0
  14. package/i18n/locales/ckb.json +24 -0
  15. package/i18n/locales/cs.json +24 -0
  16. package/i18n/locales/da.json +24 -0
  17. package/i18n/locales/de.json +24 -0
  18. package/i18n/locales/el.json +24 -0
  19. package/i18n/locales/en.json +1 -0
  20. package/i18n/locales/et.json +24 -0
  21. package/i18n/locales/fr.json +24 -0
  22. package/i18n/locales/he.json +24 -0
  23. package/i18n/locales/hi.json +24 -0
  24. package/i18n/locales/hy.json +24 -0
  25. package/i18n/locales/it.json +24 -0
  26. package/i18n/locales/ja.json +24 -0
  27. package/i18n/locales/kk.json +24 -0
  28. package/i18n/locales/km.json +24 -0
  29. package/i18n/locales/ko.json +24 -0
  30. package/i18n/locales/ky.json +24 -0
  31. package/i18n/locales/lb.json +24 -0
  32. package/i18n/locales/ms.json +24 -0
  33. package/i18n/locales/nb.json +24 -0
  34. package/i18n/locales/pl.json +24 -0
  35. package/i18n/locales/ru.json +24 -0
  36. package/i18n/locales/sl.json +24 -0
  37. package/i18n/locales/sv.json +24 -0
  38. package/i18n/locales/uk.json +24 -0
  39. package/i18n/locales/ur.json +24 -0
  40. package/i18n/locales/vi.json +24 -0
  41. package/modules/optimizeDeps.ts +45 -0
  42. package/nuxt.config.ts +1 -0
  43. package/nuxt.schema.ts +359 -0
  44. package/package.json +2 -1
  45. package/utils/content.ts +85 -10
package/nuxt.schema.ts ADDED
@@ -0,0 +1,359 @@
1
+ import { field, group } from "@nuxt/content/preview";
2
+
3
+ /**
4
+ * Nuxt Content Studio preview schema for this layer's `app.config.ts` surface.
5
+ *
6
+ * Every field below describes a key the layer itself declares and reads, so the
7
+ * schema is owned here rather than restated in each consumer. Nuxt merges
8
+ * schemas across layers, so a consumer declares only the keys it adds on top.
9
+ *
10
+ * This file is load-bearing beyond Studio: once a schema exists, Nuxt derives
11
+ * the generated `AppConfig` type from it, and any key the layer reads but the
12
+ * schema omits collapses to `{}` and fails typecheck at the read site. Adding a
13
+ * key to `app.config.ts` or reading a new one from a component means adding it
14
+ * here in the same change.
15
+ *
16
+ * Brand-free by construction: the defaults are Nuxt UI palette tokens and
17
+ * generic placeholder strings, never a product's values.
18
+ */
19
+ export default defineNuxtSchema({
20
+ appConfig: {
21
+ ui: group({
22
+ title: "UI",
23
+ description: "UI Customization.",
24
+ icon: "i-lucide-palette",
25
+ fields: {
26
+ colors: group({
27
+ title: "Colors",
28
+ description: "Manage main colors of your application",
29
+ icon: "i-lucide-palette",
30
+ fields: {
31
+ primary: field({
32
+ type: "string",
33
+ title: "Primary",
34
+ description: "Primary color of your UI.",
35
+ icon: "i-lucide-palette",
36
+ default: "green",
37
+ required: [
38
+ "red",
39
+ "orange",
40
+ "amber",
41
+ "yellow",
42
+ "lime",
43
+ "green",
44
+ "emerald",
45
+ "teal",
46
+ "cyan",
47
+ "sky",
48
+ "blue",
49
+ "indigo",
50
+ "violet",
51
+ "purple",
52
+ "fuchsia",
53
+ "pink",
54
+ "rose",
55
+ ],
56
+ }),
57
+ neutral: field({
58
+ type: "string",
59
+ title: "Neutral",
60
+ description: "Neutral color of your UI.",
61
+ icon: "i-lucide-palette",
62
+ default: "slate",
63
+ required: ["slate", "gray", "zinc", "neutral", "stone"],
64
+ }),
65
+ },
66
+ }),
67
+ icons: group({
68
+ title: "Icons",
69
+ description: "Manage icons used in the application.",
70
+ icon: "i-lucide-settings",
71
+ fields: {
72
+ search: field({
73
+ type: "icon",
74
+ title: "Search Bar",
75
+ description: "Icon to display in the search bar.",
76
+ icon: "i-lucide-search",
77
+ default: "i-lucide-search",
78
+ }),
79
+ dark: field({
80
+ type: "icon",
81
+ title: "Dark mode",
82
+ description: "Icon of color mode button for dark mode.",
83
+ icon: "i-lucide-moon",
84
+ default: "i-lucide-moon",
85
+ }),
86
+ light: field({
87
+ type: "icon",
88
+ title: "Light mode",
89
+ description: "Icon of color mode button for light mode.",
90
+ icon: "i-lucide-sun",
91
+ default: "i-lucide-sun",
92
+ }),
93
+ external: field({
94
+ type: "icon",
95
+ title: "External Link",
96
+ description: "Icon for external link.",
97
+ icon: "i-lucide-external-link",
98
+ default: "i-lucide-external-link",
99
+ }),
100
+ chevron: field({
101
+ type: "icon",
102
+ title: "Chevron",
103
+ description: "Icon for chevron.",
104
+ icon: "i-lucide-chevron-down",
105
+ default: "i-lucide-chevron-down",
106
+ }),
107
+ hash: field({
108
+ type: "icon",
109
+ title: "Hash",
110
+ description: "Icon for hash anchors.",
111
+ icon: "i-lucide-hash",
112
+ default: "i-lucide-hash",
113
+ }),
114
+ },
115
+ }),
116
+ },
117
+ }),
118
+ seo: group({
119
+ title: "SEO",
120
+ description: "SEO configuration.",
121
+ icon: "i-lucide-search",
122
+ fields: {
123
+ title: field({
124
+ type: "string",
125
+ title: "Title",
126
+ description: "Title to display in the header.",
127
+ icon: "i-lucide-type",
128
+ default: "",
129
+ }),
130
+ description: field({
131
+ type: "string",
132
+ title: "Description",
133
+ description: "Description to display in the header.",
134
+ icon: "i-lucide-type",
135
+ default: "",
136
+ }),
137
+ },
138
+ }),
139
+ header: group({
140
+ title: "Header",
141
+ description: "Header configuration.",
142
+ icon: "i-lucide-layout",
143
+ fields: {
144
+ title: field({
145
+ type: "string",
146
+ title: "Title",
147
+ description: "Title to display in the header.",
148
+ icon: "i-lucide-type",
149
+ default: "",
150
+ }),
151
+ logo: group({
152
+ title: "Logo",
153
+ description: "Header logo configuration.",
154
+ icon: "i-lucide-image",
155
+ fields: {
156
+ light: field({
157
+ type: "media",
158
+ title: "Light Mode Logo",
159
+ description: "Pick an image from your gallery.",
160
+ icon: "i-lucide-sun",
161
+ default: "",
162
+ }),
163
+ dark: field({
164
+ type: "media",
165
+ title: "Dark Mode Logo",
166
+ description: "Pick an image from your gallery.",
167
+ icon: "i-lucide-moon",
168
+ default: "",
169
+ }),
170
+ alt: field({
171
+ type: "string",
172
+ title: "Alt",
173
+ description: "Alt to display for accessibility.",
174
+ icon: "i-lucide-text",
175
+ default: "",
176
+ }),
177
+ },
178
+ }),
179
+ links: field({
180
+ type: "array",
181
+ title: "Links",
182
+ description: "Navigation links to display in the header.",
183
+ icon: "i-lucide-link",
184
+ default: [],
185
+ }),
186
+ attribution: group({
187
+ title: "Attribution",
188
+ description: "Brand attribution rendered beside the header wordmark.",
189
+ icon: "i-lucide-badge",
190
+ fields: {
191
+ prefix: field({
192
+ type: "string",
193
+ title: "Prefix",
194
+ description: 'Unlinked word before the label, e.g. "by".',
195
+ icon: "i-lucide-text",
196
+ default: "",
197
+ }),
198
+ label: field({
199
+ type: "string",
200
+ title: "Label",
201
+ description: "Linked attribution text. Empty renders nothing.",
202
+ icon: "i-lucide-type",
203
+ default: "",
204
+ }),
205
+ to: field({
206
+ type: "string",
207
+ title: "URL",
208
+ description: "Destination the label links to.",
209
+ icon: "i-lucide-link",
210
+ default: "",
211
+ }),
212
+ },
213
+ }),
214
+ },
215
+ }),
216
+ footer: group({
217
+ title: "Footer",
218
+ description: "Footer configuration.",
219
+ icon: "i-lucide-panel-bottom",
220
+ fields: {
221
+ credits: field({
222
+ type: "string",
223
+ title: "Credits",
224
+ description: "Copyright line. Falls back to the header title.",
225
+ icon: "i-lucide-copyright",
226
+ default: "",
227
+ }),
228
+ links: field({
229
+ type: "array",
230
+ title: "Links",
231
+ description: "Navigation links to display in the footer.",
232
+ icon: "i-lucide-link",
233
+ default: [],
234
+ }),
235
+ },
236
+ }),
237
+ socials: field({
238
+ type: "object",
239
+ title: "Social Networks",
240
+ description: "Social links configuration.",
241
+ icon: "i-lucide-network",
242
+ default: {},
243
+ }),
244
+ toc: group({
245
+ title: "Table of contents",
246
+ description: "TOC configuration.",
247
+ icon: "i-lucide-list",
248
+ fields: {
249
+ title: field({
250
+ type: "string",
251
+ title: "Title",
252
+ description: "Title of the table of contents.",
253
+ icon: "i-lucide-heading",
254
+ default: "On this page",
255
+ }),
256
+ bottom: group({
257
+ title: "Bottom",
258
+ description: "Bottom section of the table of contents.",
259
+ icon: "i-lucide-list",
260
+ fields: {
261
+ title: field({
262
+ type: "string",
263
+ title: "Title",
264
+ description: "Title of the bottom section.",
265
+ icon: "i-lucide-heading",
266
+ default: "Community",
267
+ }),
268
+ links: field({
269
+ type: "array",
270
+ title: "Links",
271
+ description: "Links to display in the bottom section.",
272
+ icon: "i-lucide-link",
273
+ default: [],
274
+ }),
275
+ },
276
+ }),
277
+ },
278
+ }),
279
+ analytics: group({
280
+ title: "Analytics",
281
+ description: "PostHog analytics plugin (production only, requires a runtime key).",
282
+ icon: "i-lucide-chart-line",
283
+ fields: {
284
+ enabled: field({
285
+ type: "boolean",
286
+ title: "Enabled",
287
+ description: "Set to false to opt out of the analytics plugin.",
288
+ icon: "i-lucide-toggle-right",
289
+ default: true,
290
+ }),
291
+ },
292
+ }),
293
+ i18nRedirect: group({
294
+ title: "Locale redirect",
295
+ description: "Redirect `/` to `/{locale}` (only fires when i18n is configured).",
296
+ icon: "i-lucide-languages",
297
+ fields: {
298
+ enabled: field({
299
+ type: "boolean",
300
+ title: "Enabled",
301
+ description: "Set to false to opt out of the locale redirect.",
302
+ icon: "i-lucide-toggle-right",
303
+ default: true,
304
+ }),
305
+ },
306
+ }),
307
+ nonRouteCategories: field({
308
+ type: "object",
309
+ title: "Non-route categories",
310
+ description:
311
+ "Navigation folders that group pages without owning a route, keyed by directory name.",
312
+ icon: "i-lucide-folder-tree",
313
+ default: {},
314
+ }),
315
+ docsTheme: group({
316
+ title: "Docs theme",
317
+ description: "Documentation-specific theme options.",
318
+ icon: "i-lucide-book-open",
319
+ fields: {
320
+ frameworks: field({
321
+ type: "array",
322
+ title: "Frameworks",
323
+ description:
324
+ "Frameworks offered by the sidebar select and in-content switcher, in display order. Empty by default — `defu` concatenates arrays across layers, so a non-empty default here could only be appended to, never overridden.",
325
+ icon: "i-lucide-layers",
326
+ default: [],
327
+ }),
328
+ },
329
+ }),
330
+ github: group({
331
+ title: "GitHub",
332
+ description: "GitHub configuration.",
333
+ icon: "i-simple-icons-github",
334
+ fields: {
335
+ url: field({
336
+ type: "string",
337
+ title: "URL",
338
+ description: "GitHub URL.",
339
+ icon: "i-simple-icons-github",
340
+ default: "",
341
+ }),
342
+ branch: field({
343
+ type: "string",
344
+ title: "Branch",
345
+ description: "GitHub branch.",
346
+ icon: "i-lucide-git-branch",
347
+ default: "main",
348
+ }),
349
+ rootDir: field({
350
+ type: "string",
351
+ title: "Root Directory",
352
+ description: "Root directory of the GitHub repository.",
353
+ icon: "i-lucide-folder",
354
+ default: "",
355
+ }),
356
+ },
357
+ }),
358
+ },
359
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxfront/layer-docs",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Neutral, brandable Nuxt-layer documentation theme. Consumers extend it and supply their own branding, content and section topology.",
5
5
  "keywords": [
6
6
  "docs",
@@ -25,6 +25,7 @@
25
25
  "server",
26
26
  "utils",
27
27
  "nuxt.config.ts",
28
+ "nuxt.schema.ts",
28
29
  "tsconfig.json",
29
30
  "README.md",
30
31
  "CHANGELOG.md",
package/utils/content.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { DefinedCollection } from "@nuxt/content";
2
2
  import { defineContentConfig, defineCollection, z } from "@nuxt/content";
3
3
  import { useNuxt } from "@nuxt/kit";
4
+ import { defineSitemapSchema } from "@nuxtjs/sitemap/content";
4
5
 
5
6
  /**
6
7
  * Minimal structural shape of a documentation section descriptor. The consuming
@@ -15,8 +16,11 @@ export interface DocsSectionDescriptor {
15
16
  slug: string;
16
17
  /** Human label, used as a fallback nav title. */
17
18
  label: string;
18
- /** Source folder(s) under `content/docs/`. String or list. */
19
- folder: string | string[];
19
+ /**
20
+ * Source folder(s) under `content/docs/`. String or list. Accepts a readonly
21
+ * list so a consumer can declare its topology with `as const`.
22
+ */
23
+ folder: string | readonly string[];
20
24
  /**
21
25
  * When `folder` is a list, the index whose pages mount at the section root
22
26
  * (`/docs/<slug>`) instead of `/docs/<slug>/<folder>`. Defaults to none.
@@ -24,8 +28,40 @@ export interface DocsSectionDescriptor {
24
28
  rootFolder?: number;
25
29
  }
26
30
 
27
- const createDocsSchema = () =>
31
+ /**
32
+ * Opt-in extras layered on top of the base collection set. Both default to
33
+ * `false`, so an existing `defineDocsCollections(sections)` call is unchanged.
34
+ */
35
+ export interface DefineDocsCollectionsOptions {
36
+ /**
37
+ * Fold `@nuxtjs/sitemap`'s frontmatter schema into every page collection so
38
+ * authors can set per-page sitemap fields (priority, changefreq, lastmod).
39
+ * Requires the consumer to register `@nuxtjs/sitemap`.
40
+ */
41
+ sitemap?: boolean;
42
+ /**
43
+ * Emit a locale-independent `changelog` collection sourced from
44
+ * `content/changelog/*.md`. One entry per released version, with `version`,
45
+ * `date`, and an optional `releaseUrl` override for entries that predate the
46
+ * repository's current release-tag convention.
47
+ */
48
+ changelog?: boolean;
49
+ }
50
+
51
+ // `@nuxtjs/sitemap` and `@nuxt/content` can resolve different zod majors in a
52
+ // consumer's tree (v4 and v3 respectively), and the two `ZodType` shapes are
53
+ // structurally incompatible even though the runtime value is fine. Cast at the
54
+ // single seam rather than pinning a consumer's zod.
55
+ const sitemapSchema = () =>
28
56
  z.object({
57
+ sitemap: defineSitemapSchema() as unknown as ReturnType<typeof z.any>,
58
+ });
59
+
60
+ const createLandingSchema = (options: DefineDocsCollectionsOptions) =>
61
+ options.sitemap ? sitemapSchema() : undefined;
62
+
63
+ const createDocsSchema = (options: DefineDocsCollectionsOptions) => {
64
+ const schema = z.object({
29
65
  links: z
30
66
  .array(
31
67
  z.object({
@@ -38,8 +74,18 @@ const createDocsSchema = () =>
38
74
  .optional(),
39
75
  });
40
76
 
77
+ return options.sitemap ? schema.extend(sitemapSchema().shape) : schema;
78
+ };
79
+
80
+ const createChangelogSchema = () =>
81
+ z.object({
82
+ version: z.string(),
83
+ date: z.string(),
84
+ releaseUrl: z.string().url().optional(),
85
+ });
86
+
41
87
  const buildDocsSource = (section: DocsSectionDescriptor, pathPrefix = "", urlPrefix = "") => {
42
- const folders = Array.isArray(section.folder) ? section.folder : [section.folder];
88
+ const folders = typeof section.folder === "string" ? [section.folder] : section.folder;
43
89
  const baseUrl = `${urlPrefix}/docs/${section.slug}`;
44
90
 
45
91
  if (folders.length === 1) {
@@ -64,7 +110,7 @@ const buildDocsSource = (section: DocsSectionDescriptor, pathPrefix = "", urlPre
64
110
  * ```ts
65
111
  * import { defineDocsCollections } from "@uxfront/layer-docs/content";
66
112
  * import { DOCS_SECTIONS } from "./app/constants/sections";
67
- * export default defineDocsCollections(DOCS_SECTIONS);
113
+ * export default defineDocsCollections([...DOCS_SECTIONS]);
68
114
  * ```
69
115
  *
70
116
  * Produces one `landing` collection (root markdown) plus one `docs_<key>`
@@ -72,10 +118,26 @@ const buildDocsSource = (section: DocsSectionDescriptor, pathPrefix = "", urlPre
72
118
  * collections are generated per-locale (`landing_<code>`, `docs_<key>_<code>`)
73
119
  * and sourced from a matching `content/<code>/` subtree; otherwise a single flat
74
120
  * set is produced. The section topology and content stay in the consuming app.
121
+ *
122
+ * Two opt-in extras are available — sitemap frontmatter on every page, and a
123
+ * `changelog` collection:
124
+ *
125
+ * ```ts
126
+ * export default defineDocsCollections([...DOCS_SECTIONS], {
127
+ * sitemap: true,
128
+ * changelog: true,
129
+ * });
130
+ * ```
75
131
  */
76
- export function defineDocsCollections(sections: DocsSectionDescriptor[]) {
77
- const { options } = useNuxt();
78
- const locales = options.i18n?.locales;
132
+ export function defineDocsCollections(
133
+ sections: readonly DocsSectionDescriptor[],
134
+ options: DefineDocsCollectionsOptions = {},
135
+ ) {
136
+ const { options: nuxtOptions } = useNuxt();
137
+ const locales = nuxtOptions.i18n?.locales;
138
+
139
+ const landingSchema = createLandingSchema(options);
140
+ const docsSchema = createDocsSchema(options);
79
141
 
80
142
  let collections: Record<string, DefinedCollection>;
81
143
 
@@ -87,13 +149,14 @@ export function defineDocsCollections(sections: DocsSectionDescriptor[]) {
87
149
  collections[`landing_${code}`] = defineCollection({
88
150
  type: "page",
89
151
  source: [{ include: `${code}/*.md` }],
152
+ ...(landingSchema ? { schema: landingSchema } : {}),
90
153
  });
91
154
 
92
155
  for (const section of sections) {
93
156
  collections[`docs_${section.key}_${code}`] = defineCollection({
94
157
  type: "page",
95
158
  source: buildDocsSource(section, `${code}/`, `/${code}`),
96
- schema: createDocsSchema(),
159
+ schema: docsSchema,
97
160
  });
98
161
  }
99
162
  }
@@ -102,6 +165,7 @@ export function defineDocsCollections(sections: DocsSectionDescriptor[]) {
102
165
  landing: defineCollection({
103
166
  type: "page",
104
167
  source: [{ include: "*.md" }],
168
+ ...(landingSchema ? { schema: landingSchema } : {}),
105
169
  }),
106
170
  };
107
171
 
@@ -109,10 +173,21 @@ export function defineDocsCollections(sections: DocsSectionDescriptor[]) {
109
173
  collections[`docs_${section.key}`] = defineCollection({
110
174
  type: "page",
111
175
  source: buildDocsSource(section),
112
- schema: createDocsSchema(),
176
+ schema: docsSchema,
113
177
  });
114
178
  }
115
179
  }
116
180
 
181
+ if (options.changelog) {
182
+ // Locale-independent on purpose: a release history is the same document in
183
+ // every language. The index route renders every body inline; each entry also
184
+ // has a deep-linkable detail route reading from this same collection.
185
+ collections.changelog = defineCollection({
186
+ type: "page",
187
+ source: { include: "changelog/*.md" },
188
+ schema: createChangelogSchema(),
189
+ });
190
+ }
191
+
117
192
  return defineContentConfig({ collections });
118
193
  }