livemark 0.17.0 → 0.19.0

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 (39) hide show
  1. package/build/actions/config/define.d.ts +8 -0
  2. package/build/components/Layout.js +7 -7
  3. package/build/components/SearchDialog.js +2 -3
  4. package/build/components/SidebarArticles.js +5 -4
  5. package/build/components/SidebarLinks.js +3 -3
  6. package/build/components/SidebarSections.js +6 -6
  7. package/build/components/SiteTitle.d.ts +3 -1
  8. package/build/components/SiteTitle.js +12 -4
  9. package/build/content/article.d.ts +6 -0
  10. package/build/content-collections.js +2 -3
  11. package/build/helpers/article-icon.d.ts +4 -4
  12. package/build/helpers/article-icon.js +8 -36
  13. package/build/helpers/article.d.ts +1 -1
  14. package/build/helpers/article.js +1 -1
  15. package/build/helpers/section.d.ts +42 -3
  16. package/build/helpers/section.js +34 -10
  17. package/build/helpers/section.unit.js +57 -2
  18. package/build/hooks/media.d.ts +1 -1
  19. package/build/hooks/media.js +2 -2
  20. package/build/models/article.d.ts +1 -1
  21. package/build/models/article.js +1 -1
  22. package/build/models/config.d.ts +24 -0
  23. package/build/models/section.d.ts +16 -0
  24. package/build/models/section.js +3 -1
  25. package/package.json +1 -1
  26. package/source/components/Layout.tsx +7 -7
  27. package/source/components/SearchDialog.tsx +4 -2
  28. package/source/components/SidebarArticles.tsx +4 -3
  29. package/source/components/SidebarLinks.tsx +2 -2
  30. package/source/components/SidebarSections.tsx +5 -5
  31. package/source/components/SiteTitle.tsx +12 -4
  32. package/source/content-collections.ts +1 -2
  33. package/source/helpers/article-icon.ts +7 -38
  34. package/source/helpers/article.ts +1 -1
  35. package/source/helpers/section.ts +27 -8
  36. package/source/helpers/section.unit.ts +69 -0
  37. package/source/hooks/media.ts +1 -1
  38. package/source/models/article.ts +1 -1
  39. package/source/models/section.ts +2 -0
@@ -120,6 +120,8 @@ export declare const UserConfig: z.ZodObject<{
120
120
  header: "header";
121
121
  sidebar: "sidebar";
122
122
  }>>;
123
+ siteTitle: z.ZodOptional<z.ZodString>;
124
+ siteDescription: z.ZodOptional<z.ZodString>;
123
125
  type: z.ZodLiteral<"article">;
124
126
  prefix: z.ZodString;
125
127
  }, z.core.$strip>, z.ZodObject<{
@@ -129,6 +131,8 @@ export declare const UserConfig: z.ZodObject<{
129
131
  header: "header";
130
132
  sidebar: "sidebar";
131
133
  }>>;
134
+ siteTitle: z.ZodOptional<z.ZodString>;
135
+ siteDescription: z.ZodOptional<z.ZodString>;
132
136
  type: z.ZodLiteral<"blog">;
133
137
  prefix: z.ZodString;
134
138
  }, z.core.$strip>, z.ZodObject<{
@@ -138,6 +142,8 @@ export declare const UserConfig: z.ZodObject<{
138
142
  header: "header";
139
143
  sidebar: "sidebar";
140
144
  }>>;
145
+ siteTitle: z.ZodOptional<z.ZodString>;
146
+ siteDescription: z.ZodOptional<z.ZodString>;
141
147
  type: z.ZodLiteral<"changelog">;
142
148
  prefix: z.ZodString;
143
149
  source: z.ZodString;
@@ -149,6 +155,8 @@ export declare const UserConfig: z.ZodObject<{
149
155
  header: "header";
150
156
  sidebar: "sidebar";
151
157
  }>>;
158
+ siteTitle: z.ZodOptional<z.ZodString>;
159
+ siteDescription: z.ZodOptional<z.ZodString>;
152
160
  type: z.ZodLiteral<"custom">;
153
161
  url: z.ZodString;
154
162
  }, z.core.$strip>], "type">>>>;
@@ -203,6 +211,8 @@ export declare const WebsiteConfig: z.ZodObject<{
203
211
  header: "header";
204
212
  sidebar: "sidebar";
205
213
  }>>;
214
+ siteTitle: z.ZodOptional<z.ZodString>;
215
+ siteDescription: z.ZodOptional<z.ZodString>;
206
216
  type: z.ZodLiteral<"article">;
207
217
  prefix: z.ZodString;
208
218
  }, z.core.$strip>, z.ZodObject<{
@@ -212,6 +222,8 @@ export declare const WebsiteConfig: z.ZodObject<{
212
222
  header: "header";
213
223
  sidebar: "sidebar";
214
224
  }>>;
225
+ siteTitle: z.ZodOptional<z.ZodString>;
226
+ siteDescription: z.ZodOptional<z.ZodString>;
215
227
  type: z.ZodLiteral<"blog">;
216
228
  prefix: z.ZodString;
217
229
  }, z.core.$strip>, z.ZodObject<{
@@ -221,6 +233,8 @@ export declare const WebsiteConfig: z.ZodObject<{
221
233
  header: "header";
222
234
  sidebar: "sidebar";
223
235
  }>>;
236
+ siteTitle: z.ZodOptional<z.ZodString>;
237
+ siteDescription: z.ZodOptional<z.ZodString>;
224
238
  type: z.ZodLiteral<"changelog">;
225
239
  prefix: z.ZodString;
226
240
  source: z.ZodString;
@@ -232,6 +246,8 @@ export declare const WebsiteConfig: z.ZodObject<{
232
246
  header: "header";
233
247
  sidebar: "sidebar";
234
248
  }>>;
249
+ siteTitle: z.ZodOptional<z.ZodString>;
250
+ siteDescription: z.ZodOptional<z.ZodString>;
235
251
  type: z.ZodLiteral<"custom">;
236
252
  url: z.ZodString;
237
253
  }, z.core.$strip>], "type">>>>;
@@ -352,6 +368,8 @@ export declare const Config: z.ZodObject<{
352
368
  header: "header";
353
369
  sidebar: "sidebar";
354
370
  }>>;
371
+ siteTitle: z.ZodOptional<z.ZodString>;
372
+ siteDescription: z.ZodOptional<z.ZodString>;
355
373
  type: z.ZodLiteral<"article">;
356
374
  prefix: z.ZodString;
357
375
  }, z.core.$strip>, z.ZodObject<{
@@ -361,6 +379,8 @@ export declare const Config: z.ZodObject<{
361
379
  header: "header";
362
380
  sidebar: "sidebar";
363
381
  }>>;
382
+ siteTitle: z.ZodOptional<z.ZodString>;
383
+ siteDescription: z.ZodOptional<z.ZodString>;
364
384
  type: z.ZodLiteral<"blog">;
365
385
  prefix: z.ZodString;
366
386
  }, z.core.$strip>, z.ZodObject<{
@@ -370,6 +390,8 @@ export declare const Config: z.ZodObject<{
370
390
  header: "header";
371
391
  sidebar: "sidebar";
372
392
  }>>;
393
+ siteTitle: z.ZodOptional<z.ZodString>;
394
+ siteDescription: z.ZodOptional<z.ZodString>;
373
395
  type: z.ZodLiteral<"changelog">;
374
396
  prefix: z.ZodString;
375
397
  source: z.ZodString;
@@ -381,6 +403,8 @@ export declare const Config: z.ZodObject<{
381
403
  header: "header";
382
404
  sidebar: "sidebar";
383
405
  }>>;
406
+ siteTitle: z.ZodOptional<z.ZodString>;
407
+ siteDescription: z.ZodOptional<z.ZodString>;
384
408
  type: z.ZodLiteral<"custom">;
385
409
  url: z.ZodString;
386
410
  }, z.core.$strip>], "type">>>>;
@@ -8,6 +8,8 @@ export declare const ArticleSection: z.ZodObject<{
8
8
  header: "header";
9
9
  sidebar: "sidebar";
10
10
  }>>;
11
+ siteTitle: z.ZodOptional<z.ZodString>;
12
+ siteDescription: z.ZodOptional<z.ZodString>;
11
13
  type: z.ZodLiteral<"article">;
12
14
  prefix: z.ZodString;
13
15
  }, z.core.$strip>;
@@ -20,6 +22,8 @@ export declare const BlogSection: z.ZodObject<{
20
22
  header: "header";
21
23
  sidebar: "sidebar";
22
24
  }>>;
25
+ siteTitle: z.ZodOptional<z.ZodString>;
26
+ siteDescription: z.ZodOptional<z.ZodString>;
23
27
  type: z.ZodLiteral<"blog">;
24
28
  prefix: z.ZodString;
25
29
  }, z.core.$strip>;
@@ -32,6 +36,8 @@ export declare const ChangelogSection: z.ZodObject<{
32
36
  header: "header";
33
37
  sidebar: "sidebar";
34
38
  }>>;
39
+ siteTitle: z.ZodOptional<z.ZodString>;
40
+ siteDescription: z.ZodOptional<z.ZodString>;
35
41
  type: z.ZodLiteral<"changelog">;
36
42
  prefix: z.ZodString;
37
43
  source: z.ZodString;
@@ -48,6 +54,8 @@ export declare const CustomSection: z.ZodObject<{
48
54
  header: "header";
49
55
  sidebar: "sidebar";
50
56
  }>>;
57
+ siteTitle: z.ZodOptional<z.ZodString>;
58
+ siteDescription: z.ZodOptional<z.ZodString>;
51
59
  type: z.ZodLiteral<"custom">;
52
60
  url: z.ZodString;
53
61
  }, z.core.$strip>;
@@ -64,6 +72,8 @@ export declare const Section: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodD
64
72
  header: "header";
65
73
  sidebar: "sidebar";
66
74
  }>>;
75
+ siteTitle: z.ZodOptional<z.ZodString>;
76
+ siteDescription: z.ZodOptional<z.ZodString>;
67
77
  type: z.ZodLiteral<"article">;
68
78
  prefix: z.ZodString;
69
79
  }, z.core.$strip>, z.ZodObject<{
@@ -73,6 +83,8 @@ export declare const Section: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodD
73
83
  header: "header";
74
84
  sidebar: "sidebar";
75
85
  }>>;
86
+ siteTitle: z.ZodOptional<z.ZodString>;
87
+ siteDescription: z.ZodOptional<z.ZodString>;
76
88
  type: z.ZodLiteral<"blog">;
77
89
  prefix: z.ZodString;
78
90
  }, z.core.$strip>, z.ZodObject<{
@@ -82,6 +94,8 @@ export declare const Section: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodD
82
94
  header: "header";
83
95
  sidebar: "sidebar";
84
96
  }>>;
97
+ siteTitle: z.ZodOptional<z.ZodString>;
98
+ siteDescription: z.ZodOptional<z.ZodString>;
85
99
  type: z.ZodLiteral<"changelog">;
86
100
  prefix: z.ZodString;
87
101
  source: z.ZodString;
@@ -93,6 +107,8 @@ export declare const Section: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodD
93
107
  header: "header";
94
108
  sidebar: "sidebar";
95
109
  }>>;
110
+ siteTitle: z.ZodOptional<z.ZodString>;
111
+ siteDescription: z.ZodOptional<z.ZodString>;
96
112
  type: z.ZodLiteral<"custom">;
97
113
  url: z.ZodString;
98
114
  }, z.core.$strip>], "type">>;
@@ -3,6 +3,8 @@ const BaseSection = z.object({
3
3
  icon: z.string().optional(),
4
4
  title: z.string(),
5
5
  position: z.enum(["header", "sidebar"]).default("header"),
6
+ siteTitle: z.string().optional(),
7
+ siteDescription: z.string().optional(),
6
8
  });
7
9
  export const ArticleSection = BaseSection.extend({
8
10
  type: z.literal("article"),
@@ -34,4 +36,4 @@ export const Section = z.preprocess(input => {
34
36
  ChangelogSection,
35
37
  CustomSection,
36
38
  ]));
37
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NvdXJjZS9tb2RlbHMvc2VjdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFBO0FBRXZCLE1BQU0sV0FBVyxHQUFHLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDM0IsSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLEVBQUU7SUFDM0IsS0FBSyxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7SUFDakIsUUFBUSxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxRQUFRLEVBQUUsU0FBUyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO0NBQzFELENBQUMsQ0FBQTtBQUlGLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxXQUFXLENBQUMsTUFBTSxDQUFDO0lBQy9DLElBQUksRUFBRSxDQUFDLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQztJQUMxQixNQUFNLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRTtDQUNuQixDQUFDLENBQUE7QUFJRixNQUFNLENBQUMsTUFBTSxXQUFXLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQztJQUM1QyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7SUFDdkIsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7Q0FDbkIsQ0FBQyxDQUFBO0FBSUYsTUFBTSxDQUFDLE1BQU0sZ0JBQWdCLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQztJQUNqRCxJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUM7SUFDNUIsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7SUFDbEIsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7SUFDbEIsT0FBTyxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxRQUFRLEVBQUU7Q0FDaEMsQ0FBQyxDQUFBO0FBTUYsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFHLFdBQVcsQ0FBQyxNQUFNLENBQUM7SUFDOUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO0lBQ3pCLEdBQUcsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFO0NBQ2hCLENBQUMsQ0FBQTtBQVFGLE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUMsVUFBVSxDQUNqQyxLQUFLLENBQUMsRUFBRTtJQUNOLElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3hFLE9BQU8sS0FBSyxDQUFBO0lBQ2QsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFHLEtBQWdDLENBQUE7SUFDL0MsT0FBTyxNQUFNLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxNQUFNLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFBO0FBQ25FLENBQUMsRUFDRCxDQUFDLENBQUMsa0JBQWtCLENBQUMsTUFBTSxFQUFFO0lBQzNCLGNBQWM7SUFDZCxXQUFXO0lBQ1gsZ0JBQWdCO0lBQ2hCLGFBQWE7Q0FDZCxDQUFDLENBQ0gsQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHogfSBmcm9tIFwiem9kXCJcblxuY29uc3QgQmFzZVNlY3Rpb24gPSB6Lm9iamVjdCh7XG4gIGljb246IHouc3RyaW5nKCkub3B0aW9uYWwoKSxcbiAgdGl0bGU6IHouc3RyaW5nKCksXG4gIHBvc2l0aW9uOiB6LmVudW0oW1wiaGVhZGVyXCIsIFwic2lkZWJhclwiXSkuZGVmYXVsdChcImhlYWRlclwiKSxcbn0pXG5cbi8qKiBEZWZhdWx0IHNlY3Rpb246IHJlbmRlcnMgYXJ0aWNsZXMuICovXG5leHBvcnQgdHlwZSBBcnRpY2xlU2VjdGlvbiA9IHouaW5mZXI8dHlwZW9mIEFydGljbGVTZWN0aW9uPlxuZXhwb3J0IGNvbnN0IEFydGljbGVTZWN0aW9uID0gQmFzZVNlY3Rpb24uZXh0ZW5kKHtcbiAgdHlwZTogei5saXRlcmFsKFwiYXJ0aWNsZVwiKSxcbiAgcHJlZml4OiB6LnN0cmluZygpLFxufSlcblxuLyoqIEJsb2cgc2VjdGlvbjogZGF0ZS1zb3J0ZWQgcG9zdHMgd2l0aCB0YWdzIGFuZCBpbmRleC4gKi9cbmV4cG9ydCB0eXBlIEJsb2dTZWN0aW9uID0gei5pbmZlcjx0eXBlb2YgQmxvZ1NlY3Rpb24+XG5leHBvcnQgY29uc3QgQmxvZ1NlY3Rpb24gPSBCYXNlU2VjdGlvbi5leHRlbmQoe1xuICB0eXBlOiB6LmxpdGVyYWwoXCJibG9nXCIpLFxuICBwcmVmaXg6IHouc3RyaW5nKCksXG59KVxuXG4vKiogQ2hhbmdlbG9nIHNlY3Rpb246IHNpbmdsZSBmaWxlIG9yIEdpdEh1YiByZWxlYXNlcywgc3BsaXQgcGVyIHZlcnNpb24uICovXG5leHBvcnQgdHlwZSBDaGFuZ2Vsb2dTZWN0aW9uID0gei5pbmZlcjx0eXBlb2YgQ2hhbmdlbG9nU2VjdGlvbj5cbmV4cG9ydCBjb25zdCBDaGFuZ2Vsb2dTZWN0aW9uID0gQmFzZVNlY3Rpb24uZXh0ZW5kKHtcbiAgdHlwZTogei5saXRlcmFsKFwiY2hhbmdlbG9nXCIpLFxuICBwcmVmaXg6IHouc3RyaW5nKCksXG4gIHNvdXJjZTogei5zdHJpbmcoKSxcbiAgdmVyc2lvbjogei5ib29sZWFuKCkub3B0aW9uYWwoKSxcbn0pXG5cbi8qKiBDdXN0b20gc2VjdGlvbjogcmVuZGVycyBhcyBhIGxpbmsgdG8gYW4gYXJiaXRyYXJ5IFVSTCDigJQgYWJzb2x1dGVcbiAqICAoYGh0dHBzOi8v4oCmYCkgb3IgaW50ZXJuYWwgKGAvY2hhbmdlbG9nYCkuIEludGVybmFsIFVSTHMgcGFydGljaXBhdGVcbiAqICBpbiBhY3RpdmUtc3RhdGUgaGlnaGxpZ2h0aW5nIGxpa2Ugcm91dGVkIHNlY3Rpb25zLiAqL1xuZXhwb3J0IHR5cGUgQ3VzdG9tU2VjdGlvbiA9IHouaW5mZXI8dHlwZW9mIEN1c3RvbVNlY3Rpb24+XG5leHBvcnQgY29uc3QgQ3VzdG9tU2VjdGlvbiA9IEJhc2VTZWN0aW9uLmV4dGVuZCh7XG4gIHR5cGU6IHoubGl0ZXJhbChcImN1c3RvbVwiKSxcbiAgdXJsOiB6LnN0cmluZygpLFxufSlcblxuLyoqXG4gKiBBIHNpdGUgc2VjdGlvbi4gRGlzY3JpbWluYXRlZCBvbiBgdHlwZWAuXG4gKiBXaGVuIGB0eXBlYCBpcyBvbWl0dGVkLCBpdCBkZWZhdWx0cyB0byBcImFydGljbGVcIiBzbyBwbGFpblxuICogYHsgdGl0bGUsIHByZWZpeCB9YCBlbnRyaWVzIGtlZXAgd29ya2luZy5cbiAqL1xuZXhwb3J0IHR5cGUgU2VjdGlvbiA9IHouaW5mZXI8dHlwZW9mIFNlY3Rpb24+XG5leHBvcnQgY29uc3QgU2VjdGlvbiA9IHoucHJlcHJvY2VzcyhcbiAgaW5wdXQgPT4ge1xuICAgIGlmIChpbnB1dCA9PT0gbnVsbCB8fCB0eXBlb2YgaW5wdXQgIT09IFwib2JqZWN0XCIgfHwgQXJyYXkuaXNBcnJheShpbnB1dCkpIHtcbiAgICAgIHJldHVybiBpbnB1dFxuICAgIH1cbiAgICBjb25zdCByZWNvcmQgPSBpbnB1dCBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPlxuICAgIHJldHVybiBcInR5cGVcIiBpbiByZWNvcmQgPyByZWNvcmQgOiB7IC4uLnJlY29yZCwgdHlwZTogXCJhcnRpY2xlXCIgfVxuICB9LFxuICB6LmRpc2NyaW1pbmF0ZWRVbmlvbihcInR5cGVcIiwgW1xuICAgIEFydGljbGVTZWN0aW9uLFxuICAgIEJsb2dTZWN0aW9uLFxuICAgIENoYW5nZWxvZ1NlY3Rpb24sXG4gICAgQ3VzdG9tU2VjdGlvbixcbiAgXSksXG4pXG4iXX0=
39
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NvdXJjZS9tb2RlbHMvc2VjdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFBO0FBRXZCLE1BQU0sV0FBVyxHQUFHLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDM0IsSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLEVBQUU7SUFDM0IsS0FBSyxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7SUFDakIsUUFBUSxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxRQUFRLEVBQUUsU0FBUyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO0lBQ3pELFNBQVMsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ2hDLGVBQWUsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxFQUFFO0NBQ3ZDLENBQUMsQ0FBQTtBQUlGLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxXQUFXLENBQUMsTUFBTSxDQUFDO0lBQy9DLElBQUksRUFBRSxDQUFDLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQztJQUMxQixNQUFNLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRTtDQUNuQixDQUFDLENBQUE7QUFJRixNQUFNLENBQUMsTUFBTSxXQUFXLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQztJQUM1QyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7SUFDdkIsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7Q0FDbkIsQ0FBQyxDQUFBO0FBSUYsTUFBTSxDQUFDLE1BQU0sZ0JBQWdCLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQztJQUNqRCxJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUM7SUFDNUIsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7SUFDbEIsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUU7SUFDbEIsT0FBTyxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxRQUFRLEVBQUU7Q0FDaEMsQ0FBQyxDQUFBO0FBTUYsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFHLFdBQVcsQ0FBQyxNQUFNLENBQUM7SUFDOUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO0lBQ3pCLEdBQUcsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFO0NBQ2hCLENBQUMsQ0FBQTtBQVFGLE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUMsVUFBVSxDQUNqQyxLQUFLLENBQUMsRUFBRTtJQUNOLElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3hFLE9BQU8sS0FBSyxDQUFBO0lBQ2QsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFHLEtBQWdDLENBQUE7SUFDL0MsT0FBTyxNQUFNLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxNQUFNLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFBO0FBQ25FLENBQUMsRUFDRCxDQUFDLENBQUMsa0JBQWtCLENBQUMsTUFBTSxFQUFFO0lBQzNCLGNBQWM7SUFDZCxXQUFXO0lBQ1gsZ0JBQWdCO0lBQ2hCLGFBQWE7Q0FDZCxDQUFDLENBQ0gsQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHogfSBmcm9tIFwiem9kXCJcblxuY29uc3QgQmFzZVNlY3Rpb24gPSB6Lm9iamVjdCh7XG4gIGljb246IHouc3RyaW5nKCkub3B0aW9uYWwoKSxcbiAgdGl0bGU6IHouc3RyaW5nKCksXG4gIHBvc2l0aW9uOiB6LmVudW0oW1wiaGVhZGVyXCIsIFwic2lkZWJhclwiXSkuZGVmYXVsdChcImhlYWRlclwiKSxcbiAgc2l0ZVRpdGxlOiB6LnN0cmluZygpLm9wdGlvbmFsKCksXG4gIHNpdGVEZXNjcmlwdGlvbjogei5zdHJpbmcoKS5vcHRpb25hbCgpLFxufSlcblxuLyoqIERlZmF1bHQgc2VjdGlvbjogcmVuZGVycyBhcnRpY2xlcy4gKi9cbmV4cG9ydCB0eXBlIEFydGljbGVTZWN0aW9uID0gei5pbmZlcjx0eXBlb2YgQXJ0aWNsZVNlY3Rpb24+XG5leHBvcnQgY29uc3QgQXJ0aWNsZVNlY3Rpb24gPSBCYXNlU2VjdGlvbi5leHRlbmQoe1xuICB0eXBlOiB6LmxpdGVyYWwoXCJhcnRpY2xlXCIpLFxuICBwcmVmaXg6IHouc3RyaW5nKCksXG59KVxuXG4vKiogQmxvZyBzZWN0aW9uOiBkYXRlLXNvcnRlZCBwb3N0cyB3aXRoIHRhZ3MgYW5kIGluZGV4LiAqL1xuZXhwb3J0IHR5cGUgQmxvZ1NlY3Rpb24gPSB6LmluZmVyPHR5cGVvZiBCbG9nU2VjdGlvbj5cbmV4cG9ydCBjb25zdCBCbG9nU2VjdGlvbiA9IEJhc2VTZWN0aW9uLmV4dGVuZCh7XG4gIHR5cGU6IHoubGl0ZXJhbChcImJsb2dcIiksXG4gIHByZWZpeDogei5zdHJpbmcoKSxcbn0pXG5cbi8qKiBDaGFuZ2Vsb2cgc2VjdGlvbjogc2luZ2xlIGZpbGUgb3IgR2l0SHViIHJlbGVhc2VzLCBzcGxpdCBwZXIgdmVyc2lvbi4gKi9cbmV4cG9ydCB0eXBlIENoYW5nZWxvZ1NlY3Rpb24gPSB6LmluZmVyPHR5cGVvZiBDaGFuZ2Vsb2dTZWN0aW9uPlxuZXhwb3J0IGNvbnN0IENoYW5nZWxvZ1NlY3Rpb24gPSBCYXNlU2VjdGlvbi5leHRlbmQoe1xuICB0eXBlOiB6LmxpdGVyYWwoXCJjaGFuZ2Vsb2dcIiksXG4gIHByZWZpeDogei5zdHJpbmcoKSxcbiAgc291cmNlOiB6LnN0cmluZygpLFxuICB2ZXJzaW9uOiB6LmJvb2xlYW4oKS5vcHRpb25hbCgpLFxufSlcblxuLyoqIEN1c3RvbSBzZWN0aW9uOiByZW5kZXJzIGFzIGEgbGluayB0byBhbiBhcmJpdHJhcnkgVVJMIOKAlCBhYnNvbHV0ZVxuICogIChgaHR0cHM6Ly/igKZgKSBvciBpbnRlcm5hbCAoYC9jaGFuZ2Vsb2dgKS4gSW50ZXJuYWwgVVJMcyBwYXJ0aWNpcGF0ZVxuICogIGluIGFjdGl2ZS1zdGF0ZSBoaWdobGlnaHRpbmcgbGlrZSByb3V0ZWQgc2VjdGlvbnMuICovXG5leHBvcnQgdHlwZSBDdXN0b21TZWN0aW9uID0gei5pbmZlcjx0eXBlb2YgQ3VzdG9tU2VjdGlvbj5cbmV4cG9ydCBjb25zdCBDdXN0b21TZWN0aW9uID0gQmFzZVNlY3Rpb24uZXh0ZW5kKHtcbiAgdHlwZTogei5saXRlcmFsKFwiY3VzdG9tXCIpLFxuICB1cmw6IHouc3RyaW5nKCksXG59KVxuXG4vKipcbiAqIEEgc2l0ZSBzZWN0aW9uLiBEaXNjcmltaW5hdGVkIG9uIGB0eXBlYC5cbiAqIFdoZW4gYHR5cGVgIGlzIG9taXR0ZWQsIGl0IGRlZmF1bHRzIHRvIFwiYXJ0aWNsZVwiIHNvIHBsYWluXG4gKiBgeyB0aXRsZSwgcHJlZml4IH1gIGVudHJpZXMga2VlcCB3b3JraW5nLlxuICovXG5leHBvcnQgdHlwZSBTZWN0aW9uID0gei5pbmZlcjx0eXBlb2YgU2VjdGlvbj5cbmV4cG9ydCBjb25zdCBTZWN0aW9uID0gei5wcmVwcm9jZXNzKFxuICBpbnB1dCA9PiB7XG4gICAgaWYgKGlucHV0ID09PSBudWxsIHx8IHR5cGVvZiBpbnB1dCAhPT0gXCJvYmplY3RcIiB8fCBBcnJheS5pc0FycmF5KGlucHV0KSkge1xuICAgICAgcmV0dXJuIGlucHV0XG4gICAgfVxuICAgIGNvbnN0IHJlY29yZCA9IGlucHV0IGFzIFJlY29yZDxzdHJpbmcsIHVua25vd24+XG4gICAgcmV0dXJuIFwidHlwZVwiIGluIHJlY29yZCA/IHJlY29yZCA6IHsgLi4ucmVjb3JkLCB0eXBlOiBcImFydGljbGVcIiB9XG4gIH0sXG4gIHouZGlzY3JpbWluYXRlZFVuaW9uKFwidHlwZVwiLCBbXG4gICAgQXJ0aWNsZVNlY3Rpb24sXG4gICAgQmxvZ1NlY3Rpb24sXG4gICAgQ2hhbmdlbG9nU2VjdGlvbixcbiAgICBDdXN0b21TZWN0aW9uLFxuICBdKSxcbilcbiJdfQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livemark",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Livemark description",
5
5
  "license": "MIT",
6
6
  "author": "Evgeny Karev",
@@ -9,7 +9,7 @@ import {
9
9
  } from "../content/article.ts"
10
10
  import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
11
11
  import { prefixUrl } from "../helpers/prefix-url.ts"
12
- import { customSectionActive, sectionIcon } from "../helpers/section.ts"
12
+ import { customSectionActive } from "../helpers/section.ts"
13
13
  import {
14
14
  SidebarInset,
15
15
  SidebarProvider,
@@ -47,14 +47,14 @@ export function Layout(props: {
47
47
  const header = (
48
48
  <header className="sticky top-0 z-20 flex h-16 shrink-0 items-center border-b bg-background">
49
49
  <div
50
- className={`flex items-center self-stretch pl-4 ${props.withSidebar ? "pr-4" : "md:pr-20"}`}
50
+ className={`flex items-center self-stretch pl-4 ${props.withSidebar ? "pr-4" : "lg:pr-20"}`}
51
51
  >
52
52
  {props.withSidebar ? (
53
53
  <>
54
- <div className="hidden md:flex">
54
+ <div className="hidden lg:flex">
55
55
  <SidebarTrigger />
56
56
  </div>
57
- <Link to="/" className="md:hidden flex items-center gap-2">
57
+ <Link to="/" className="lg:hidden flex items-center gap-2">
58
58
  <SiteTitle />
59
59
  </Link>
60
60
  </>
@@ -64,12 +64,12 @@ export function Layout(props: {
64
64
  </Link>
65
65
  )}
66
66
  </div>
67
- <div className="hidden md:flex flex-1 items-center gap-8 self-stretch px-6 text-sm">
67
+ <div className="hidden lg:flex flex-1 items-center gap-8 self-stretch px-6 text-sm">
68
68
  {sections?.length ? (
69
69
  sections
70
70
  .filter(section => section.position === "header")
71
71
  .map(section => {
72
- const icon = sectionIcon(section)
72
+ const icon = section.icon
73
73
  if (section.type === "custom") {
74
74
  const isActive = customSectionActive(section.url, pathname)
75
75
  return (
@@ -151,7 +151,7 @@ export function Layout(props: {
151
151
  ))}
152
152
  <Banner />
153
153
  </div>
154
- <div className="md:hidden ml-auto pr-2">
154
+ <div className="lg:hidden ml-auto pr-2">
155
155
  <SidebarTrigger />
156
156
  </div>
157
157
  </header>
@@ -15,7 +15,7 @@ import {
15
15
  interface SearchResult {
16
16
  title: string
17
17
  path: string
18
- icon: string
18
+ icon?: string
19
19
  snippet: string
20
20
  group: string
21
21
  }
@@ -157,7 +157,9 @@ export function SearchDialog(props: {
157
157
  onSelect={() => handleSelect(result.path)}
158
158
  className="py-2.5"
159
159
  >
160
- <Icon className="size-5 shrink-0 mt-0.5 self-start opacity-60" />
160
+ {Icon && (
161
+ <Icon className="size-5 shrink-0 mt-0.5 self-start opacity-60" />
162
+ )}
161
163
  <div className="flex flex-col gap-0.5 min-w-0">
162
164
  <span className="text-base font-medium">
163
165
  {result.title}
@@ -56,7 +56,8 @@ function splatFor(pathname: string) {
56
56
  }
57
57
 
58
58
  function isActive(articlePathname: string, currentPath: string) {
59
- const normalized = `/${currentPath.replace(/^\/|\/$/g, "")}/`
59
+ const trimmed = currentPath.replace(/^\/|\/$/g, "")
60
+ const normalized = trimmed ? `/${trimmed}/` : "/"
60
61
  return articlePathname === normalized
61
62
  }
62
63
 
@@ -83,7 +84,7 @@ function NavNode(props: {
83
84
  className={active ? "" : "opacity-75"}
84
85
  render={link}
85
86
  >
86
- {!isSub && <Icon className="size-4" />}
87
+ {!isSub && Icon && <Icon className="size-4" />}
87
88
  {label}
88
89
  </Button>
89
90
  </Item>
@@ -98,7 +99,7 @@ function NavNode(props: {
98
99
  className={active ? "" : "opacity-75"}
99
100
  render={link}
100
101
  >
101
- {!isSub && <Icon className="size-4" />}
102
+ {!isSub && Icon && <Icon className="size-4" />}
102
103
  {label}
103
104
  </Button>
104
105
  <CollapsibleTrigger
@@ -27,7 +27,7 @@ export function SidebarLinks() {
27
27
  const hasDesktopItems = links.some(l => l.position === "sidebar")
28
28
 
29
29
  return (
30
- <SidebarGroup className={hasDesktopItems ? undefined : "md:hidden"}>
30
+ <SidebarGroup className={hasDesktopItems ? undefined : "lg:hidden"}>
31
31
  <SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
32
32
  Links
33
33
  </SidebarGroupLabel>
@@ -38,7 +38,7 @@ export function SidebarLinks() {
38
38
  return (
39
39
  <SidebarMenuItem
40
40
  key={link.url}
41
- className={mobileOnly ? "md:hidden" : undefined}
41
+ className={mobileOnly ? "lg:hidden" : undefined}
42
42
  >
43
43
  <SidebarMenuButton
44
44
  className="text-muted-foreground"
@@ -3,7 +3,7 @@ import { Link, useLocation, useMatch } from "@tanstack/react-router"
3
3
  import { currentSection, sectionFirstArticle } from "../content/article.ts"
4
4
  import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
5
5
  import { prefixUrl } from "../helpers/prefix-url.ts"
6
- import { customSectionActive, sectionIcon } from "../helpers/section.ts"
6
+ import { customSectionActive } from "../helpers/section.ts"
7
7
  import {
8
8
  SidebarGroup,
9
9
  SidebarGroupContent,
@@ -32,7 +32,7 @@ export function SidebarSections() {
32
32
  const hasDesktopItems = sections.some(s => s.position === "sidebar")
33
33
 
34
34
  return (
35
- <SidebarGroup className={hasDesktopItems ? undefined : "md:hidden"}>
35
+ <SidebarGroup className={hasDesktopItems ? undefined : "lg:hidden"}>
36
36
  <SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
37
37
  Sections
38
38
  </SidebarGroupLabel>
@@ -40,13 +40,13 @@ export function SidebarSections() {
40
40
  <SidebarMenu>
41
41
  {sections.map(s => {
42
42
  const mobileOnly = s.position !== "sidebar"
43
- const icon = sectionIcon(s)
43
+ const icon = s.icon
44
44
  if (s.type === "custom") {
45
45
  const active = customSectionActive(s.url, pathname)
46
46
  return (
47
47
  <SidebarMenuItem
48
48
  key={s.url}
49
- className={mobileOnly ? "md:hidden" : undefined}
49
+ className={mobileOnly ? "lg:hidden" : undefined}
50
50
  >
51
51
  <SidebarMenuButton
52
52
  isActive={active}
@@ -67,7 +67,7 @@ export function SidebarSections() {
67
67
  return (
68
68
  <SidebarMenuItem
69
69
  key={s.prefix}
70
- className={mobileOnly ? "md:hidden" : undefined}
70
+ className={mobileOnly ? "lg:hidden" : undefined}
71
71
  >
72
72
  <SidebarMenuButton
73
73
  isActive={active}
@@ -1,16 +1,24 @@
1
1
  import { config } from "livemark:virtual"
2
+ import { useLocation } from "@tanstack/react-router"
2
3
  import { prefixUrl } from "../helpers/prefix-url.ts"
4
+ import { activeSiteSection } from "../helpers/section.ts"
3
5
  import defaultLogo from "../assets/logo.svg"
4
6
 
5
- /** Renders the site logo, title, and description in the sidebar header */
7
+ /** Renders the site logo, title, and description in the sidebar header.
8
+ * When the current pathname falls inside a section that defines its own
9
+ * `siteTitle` / `siteDescription`, those override the global config. */
6
10
  export function SiteTitle() {
11
+ const pathname = useLocation({ select: l => l.pathname })
12
+ const section = activeSiteSection(pathname, config.sections ?? [])
13
+ const title = section?.siteTitle ?? config.title
14
+ const description = section?.siteDescription ?? config.description
7
15
  const logo = prefixUrl(config.logo ?? defaultLogo, config.base)
8
16
  return (
9
17
  <div className="flex items-end gap-2.5 text-sm">
10
- <img src={logo} alt={config.title} className="size-6.5 mb-1.25" />
18
+ <img src={logo} alt={title} className="size-6.5 mb-1.25" />
11
19
  <div className="flex flex-col gap-0.5 leading-none">
12
- <span className="font-semibold">{config.title}</span>
13
- <span className="text-xs opacity-80">{config.description}</span>
20
+ <span className="font-semibold">{title}</span>
21
+ <span className="text-xs opacity-80">{description}</span>
14
22
  </div>
15
23
  </div>
16
24
  )
@@ -16,7 +16,6 @@ import remarkMath from "remark-math"
16
16
  import { loadConfig } from "./actions/config/load.ts"
17
17
  import { buildChangelog, cacheIncludeGlob } from "./actions/changelog/build.ts"
18
18
  import { Article } from "./models/article.ts"
19
- import { pickDefaultIcon } from "./helpers/article-icon.ts"
20
19
  import slugify from "@sindresorhus/slugify"
21
20
  import remarkCustomHeadingId from "./plugins/remark-custom-heading-id.ts"
22
21
  import { remarkCallout } from "./plugins/remark-callout.ts"
@@ -128,7 +127,7 @@ const articles = defineCollection({
128
127
  const filePath = resolve(config.root, doc._meta.filePath)
129
128
  const content = resolveIncludes(doc.content, filePath)
130
129
  const title = doc.title ?? extractTitle(content, doc._meta.path)
131
- const icon = doc.icon ?? pickDefaultIcon(doc._meta.path)
130
+ const icon = doc.icon
132
131
  const path = doc.path ?? toPath(doc._meta.filePath)
133
132
  // The article's h1 is rendered from `title` by the layout, so strip any
134
133
  // leading h1 in the body to avoid two h1s side-by-side (especially when
@@ -1,43 +1,12 @@
1
- import { FileText, icons } from "lucide-react"
1
+ import { icons } from "lucide-react"
2
2
 
3
- /** Resolve a Lucide icon by kebab-case name, falling back to FileText. */
4
- export function resolveArticleIcon(name: string) {
3
+ /** Resolve a Lucide icon by kebab-case name. Returns undefined when no
4
+ * name is given or the name doesn't match a known icon — articles
5
+ * without an explicit `icon` frontmatter render with no icon at all. */
6
+ export function resolveArticleIcon(name: string | undefined) {
7
+ if (!name) return undefined
5
8
  const key = toPascalCase(name)
6
- return key in icons ? icons[key as keyof typeof icons] : FileText
7
- }
8
-
9
- /** Curated set of icons used as default fallbacks for articles without one. */
10
- const defaultIconNames = [
11
- "binary",
12
- "blocks",
13
- "brain-circuit",
14
- "bug",
15
- "cloud",
16
- "code",
17
- "cpu",
18
- "database",
19
- "file-code",
20
- "git-branch",
21
- "globe",
22
- "hard-drive",
23
- "layers",
24
- "network",
25
- "rocket",
26
- "server",
27
- "shield",
28
- "sliders-horizontal",
29
- "terminal",
30
- "webhook",
31
- "zap",
32
- ]
33
-
34
- /** Deterministic icon name based on article path */
35
- export function pickDefaultIcon(path: string) {
36
- let hash = 0
37
- for (let i = 0; i < path.length; i++) {
38
- hash = (hash * 31 + path.charCodeAt(i)) | 0
39
- }
40
- return defaultIconNames[Math.abs(hash) % defaultIconNames.length]!
9
+ return key in icons ? icons[key as keyof typeof icons] : undefined
41
10
  }
42
11
 
43
12
  function toPascalCase(name: string) {
@@ -6,7 +6,7 @@ export function buildArticleTree(
6
6
  path: string
7
7
  title: string
8
8
  label?: string
9
- icon: string
9
+ icon?: string
10
10
  }[],
11
11
  ) {
12
12
  const nodeMap = new Map<string, ArticleNode>()
@@ -1,15 +1,34 @@
1
1
  import type { Section } from "../models/section.ts"
2
2
 
3
- const DEFAULT_ICONS: Partial<Record<Section["type"], string>> = {
4
- article: "book-open",
5
- blog: "rss",
6
- changelog: "history",
3
+ /** Return the section whose prefix or internal-URL best matches the
4
+ * given pathname. Routed sections (article/blog/changelog) match by
5
+ * `prefix`. Custom sections with internal URLs (`/`-leading, not
6
+ * `//`) match by their URL — with the same exact-`/` guard as
7
+ * `customSectionActive` so a custom URL of `/` only activates on
8
+ * the home page itself. Custom sections with absolute URLs never
9
+ * match. Longest match wins. */
10
+ export function activeSiteSection(pathname: string, sections: Section[]) {
11
+ const current = `/${pathname.replace(/^\/|\/$/g, "")}/`
12
+ let best: Section | undefined
13
+ let bestLen = 0
14
+ for (const s of sections) {
15
+ const prefix = sectionMatchPrefix(s)
16
+ if (!prefix) continue
17
+ if (s.type === "custom" && prefix === "/" && current !== "/") continue
18
+ if (!current.startsWith(prefix)) continue
19
+ if (prefix.length > bestLen) {
20
+ best = s
21
+ bestLen = prefix.length
22
+ }
23
+ }
24
+ return best
7
25
  }
8
26
 
9
- /** Return the section's icon, falling back to a type-based default.
10
- * Custom sections have no default — the user opts in via `icon`. */
11
- export function sectionIcon(section: Pick<Section, "icon" | "type">) {
12
- return section.icon ?? DEFAULT_ICONS[section.type]
27
+ function sectionMatchPrefix(s: Section): string | undefined {
28
+ if (s.type !== "custom") return s.prefix
29
+ if (!s.url.startsWith("/") || s.url.startsWith("//")) return undefined
30
+ const stripped = s.url.split(/[?#]/)[0] ?? ""
31
+ return `/${stripped.replace(/^\/|\/$/g, "")}/`
13
32
  }
14
33
 
15
34
  /** Whether a custom section's URL should highlight as active for the
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, it } from "vite-plus/test"
2
+ import type { Section } from "../models/section.ts"
2
3
  import {
4
+ activeSiteSection,
3
5
  customSectionActive,
4
6
  matchSection,
5
7
  partitionBySection,
@@ -111,3 +113,70 @@ describe("customSectionActive", () => {
111
113
  expect(customSectionActive("/about#top", "/about/team/")).toBe(true)
112
114
  })
113
115
  })
116
+
117
+ describe("activeSiteSection", () => {
118
+ const docs: Section = {
119
+ type: "article",
120
+ title: "Docs",
121
+ prefix: "/",
122
+ position: "header",
123
+ }
124
+ const blog: Section = {
125
+ type: "blog",
126
+ title: "Blog",
127
+ prefix: "/blog/",
128
+ position: "header",
129
+ }
130
+ const apiCustom: Section = {
131
+ type: "custom",
132
+ title: "API",
133
+ url: "/api/v2",
134
+ position: "header",
135
+ }
136
+ const githubCustom: Section = {
137
+ type: "custom",
138
+ title: "GitHub",
139
+ url: "https://github.com/example/repo",
140
+ position: "header",
141
+ }
142
+ const homeCustom: Section = {
143
+ type: "custom",
144
+ title: "Home",
145
+ url: "/",
146
+ position: "header",
147
+ }
148
+
149
+ it("should return the routed section matching by prefix", () => {
150
+ expect(activeSiteSection("/blog/post/", [docs, blog])?.title).toBe("Blog")
151
+ })
152
+
153
+ it("should return the custom section matching by internal url", () => {
154
+ expect(activeSiteSection("/api/v2/users/", [docs, apiCustom])?.title).toBe(
155
+ "API",
156
+ )
157
+ })
158
+
159
+ it("should pick the longest match among overlapping prefixes", () => {
160
+ expect(activeSiteSection("/blog/post/", [docs, blog])?.title).toBe("Blog")
161
+ expect(activeSiteSection("/api/v2/foo/", [docs, apiCustom])?.title).toBe(
162
+ "API",
163
+ )
164
+ })
165
+
166
+ it("should ignore custom sections with absolute URLs", () => {
167
+ expect(activeSiteSection("/anything/", [githubCustom])).toBeUndefined()
168
+ })
169
+
170
+ it("should match custom URL `/` only on the home page", () => {
171
+ expect(activeSiteSection("/", [homeCustom])?.title).toBe("Home")
172
+ expect(activeSiteSection("/about/", [homeCustom])).toBeUndefined()
173
+ })
174
+
175
+ it("should let routed `/` match descendants while custom `/` does not", () => {
176
+ expect(activeSiteSection("/about/", [docs, homeCustom])?.title).toBe("Docs")
177
+ })
178
+
179
+ it("should return undefined when no section matches", () => {
180
+ expect(activeSiteSection("/anything/", [blog, apiCustom])).toBeUndefined()
181
+ })
182
+ })
@@ -1,6 +1,6 @@
1
1
  import { useSyncExternalStore } from "react"
2
2
 
3
- export const MOBILE_BREAKPOINT = 768
3
+ export const MOBILE_BREAKPOINT = 1024
4
4
 
5
5
  export function useIsMobile() {
6
6
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
@@ -45,6 +45,6 @@ export interface ArticleNode {
45
45
  path: string
46
46
  title: string
47
47
  label?: string
48
- icon: string
48
+ icon?: string
49
49
  children: ArticleNode[]
50
50
  }
@@ -4,6 +4,8 @@ const BaseSection = z.object({
4
4
  icon: z.string().optional(),
5
5
  title: z.string(),
6
6
  position: z.enum(["header", "sidebar"]).default("header"),
7
+ siteTitle: z.string().optional(),
8
+ siteDescription: z.string().optional(),
7
9
  })
8
10
 
9
11
  /** Default section: renders articles. */