starlight-sidebar-topics-dropdown 0.2.2 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # starlight-sidebar-topics-dropdown
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#16](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/16) [`9edb5dc`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/9edb5dca0215df684c4471ed7e1fe878617be91a) Thanks [@trueberryless](https://github.com/trueberryless)! - Adds support for Astro v5, drops support for Astro v4.
8
+
9
+ ⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now `0.30.0`.
10
+
11
+ Please follow the [upgrade guide](https://github.com/withastro/starlight/releases/tag/%40astrojs/starlight%400.30.0) to update your project.
12
+
13
+ Note that the [`legacy.collections` flag](https://docs.astro.build/en/reference/legacy-flags/#collections) is not supported by this plugin and you should update your collections to use Astro's new Content Layer API.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#18](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/18) [`2039e9a`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/2039e9a60659b5628c7406952bde0d453e38aeda) Thanks [@trueberryless](https://github.com/trueberryless)! - Add Unnested Sidebar configuration
18
+
3
19
  ## 0.2.2
4
20
 
5
21
  ### Patch Changes
package/index.ts CHANGED
@@ -1,82 +1,92 @@
1
- import type { StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types";
1
+ import type {
2
+ StarlightPlugin,
3
+ StarlightUserConfig,
4
+ } from "@astrojs/starlight/types";
2
5
 
3
6
  import {
4
- StarlightSidebarTopicsDropdownConfigSchema,
5
- type StarlightSidebarTopicsDropdownUserConfig,
7
+ StarlightSidebarTopicsDropdownConfigSchema,
8
+ type StarlightSidebarTopicsDropdownUserConfig,
6
9
  } from "./lib/config";
7
10
  import { overrideStarlightComponent, throwPluginError } from "./lib/plugin";
8
11
  import { vitePluginStarlightSidebarTopicsDropdown } from "./lib/vite";
9
12
 
10
13
  export type {
11
- StarlightSidebarTopicsDropdownConfig,
12
- StarlightSidebarTopicsDropdownUserConfig,
14
+ StarlightSidebarTopicsDropdownConfig,
15
+ StarlightSidebarTopicsDropdownUserConfig,
13
16
  } from "./lib/config";
14
17
 
15
18
  export default function starlightSidebarTopicsDropdownPlugin(
16
- userConfig: StarlightSidebarTopicsDropdownUserConfig
19
+ userConfig: StarlightSidebarTopicsDropdownUserConfig
17
20
  ): StarlightPlugin {
18
- const parsedConfig = StarlightSidebarTopicsDropdownConfigSchema.safeParse(userConfig);
21
+ const parsedConfig =
22
+ StarlightSidebarTopicsDropdownConfigSchema.safeParse(userConfig);
19
23
 
20
- if (!parsedConfig.success) {
21
- throwPluginError(
22
- `The provided plugin configuration is invalid.\n${parsedConfig.error.issues
23
- .map((issue) => issue.message)
24
- .join("\n")}`
25
- );
26
- }
24
+ if (!parsedConfig.success) {
25
+ throwPluginError(
26
+ `The provided plugin configuration is invalid.\n${parsedConfig.error.issues
27
+ .map((issue) => issue.message)
28
+ .join("\n")}`
29
+ );
30
+ }
27
31
 
28
- const config = parsedConfig.data;
32
+ const config = parsedConfig.data;
29
33
 
30
- return {
31
- name: "starlight-sidebar-topics-dropdown-plugin",
32
- hooks: {
33
- setup({ addIntegration, command, config: starlightConfig, logger, updateConfig }) {
34
- if (command !== "dev" && command !== "build") return;
34
+ return {
35
+ name: "starlight-sidebar-topics-dropdown-plugin",
36
+ hooks: {
37
+ setup({
38
+ addIntegration,
39
+ command,
40
+ config: starlightConfig,
41
+ logger,
42
+ updateConfig,
43
+ }) {
44
+ if (command !== "dev" && command !== "build") return;
35
45
 
36
- if (starlightConfig.sidebar) {
37
- throwPluginError(
38
- "It looks like you have a `sidebar` configured in your Starlight configuration. To use `starlight-sidebar-topics-dropdown`, create a new topic with your sidebar items.",
39
- "Learn more about topic configuration at https://starlight-sidebar-topics-dropdown.netlify.app/docs/configuration/"
40
- );
41
- }
46
+ if (starlightConfig.sidebar) {
47
+ throwPluginError(
48
+ "It looks like you have a `sidebar` configured in your Starlight configuration. To use `starlight-sidebar-topics-dropdown`, create a new topic with your sidebar items.",
49
+ "Learn more about topic configuration at https://starlight-sidebar-topics-dropdown.netlify.app/docs/configuration/"
50
+ );
51
+ }
42
52
 
43
- const sidebar: StarlightUserConfig["sidebar"] = [];
53
+ const sidebar: StarlightUserConfig["sidebar"] = [];
44
54
 
45
- for (const [index, topic] of config.entries()) {
46
- if ("items" in topic) {
47
- sidebar.push({ label: String(index), items: topic.items });
48
- }
49
- }
50
- updateConfig({
51
- components: {
52
- ...starlightConfig.components,
53
- ...overrideStarlightComponent(
54
- starlightConfig.components,
55
- logger,
56
- "Sidebar"
57
- ),
58
- ...overrideStarlightComponent(
59
- starlightConfig.components,
60
- logger,
61
- "Pagination"
62
- ),
63
- },
64
- sidebar,
65
- });
55
+ for (const [index, topic] of config.entries()) {
56
+ if ("items" in topic) {
57
+ sidebar.push({ label: String(index), items: topic.items });
58
+ }
59
+ }
60
+ updateConfig({
61
+ components: {
62
+ ...starlightConfig.components,
63
+ ...overrideStarlightComponent(
64
+ starlightConfig.components,
65
+ logger,
66
+ "Sidebar"
67
+ ),
68
+ ...overrideStarlightComponent(
69
+ starlightConfig.components,
70
+ logger,
71
+ "Pagination"
72
+ ),
73
+ },
74
+ sidebar,
75
+ });
66
76
 
67
- addIntegration({
68
- name: "starlight-sidebar-topics-dropdown-integration",
69
- hooks: {
70
- "astro:config:setup": ({ updateConfig }) => {
71
- updateConfig({
72
- vite: {
73
- plugins: [vitePluginStarlightSidebarTopicsDropdown(config)],
74
- },
75
- });
76
- },
77
- },
78
- });
77
+ addIntegration({
78
+ name: "starlight-sidebar-topics-dropdown-integration",
79
+ hooks: {
80
+ "astro:config:setup": ({ updateConfig }) => {
81
+ updateConfig({
82
+ vite: {
83
+ plugins: [vitePluginStarlightSidebarTopicsDropdown(config)],
84
+ },
85
+ });
79
86
  },
80
- },
81
- };
87
+ },
88
+ });
89
+ },
90
+ },
91
+ };
82
92
  }
package/lib/config.ts CHANGED
@@ -2,73 +2,81 @@ import type { StarlightUserConfig } from "@astrojs/starlight/types";
2
2
  import { z } from "astro/zod";
3
3
 
4
4
  const sidebarTopicBadgeSchema = z.object({
5
- text: z.union([z.string(), z.record(z.string())]),
6
- variant: z.enum(["note", "danger", "success", "caution", "tip", "default"]).default("default"),
5
+ text: z.union([z.string(), z.record(z.string())]),
6
+ variant: z
7
+ .enum(["note", "danger", "success", "caution", "tip", "default"])
8
+ .default("default"),
7
9
  });
8
10
 
9
11
  const sidebarTopicBaseSchema = z.object({
10
- /**
11
- * An optional badge to display next to the topic label.
12
- *
13
- * This option accepts the same configuration as the Starlight badge sidebar item configuration.
14
- * @see https://starlight.astro.build/guides/sidebar/#badges
15
- */
16
- badge: z
17
- .union([z.string(), sidebarTopicBadgeSchema])
18
- .transform((badge) =>
19
- typeof badge === "string" ? { variant: "default" as const, text: badge } : badge
20
- )
21
- .optional(),
22
- /**
23
- * The name of an optional icon to display before the topic label set to one of Starlight’s built-in icons.
24
- * @see https://starlight.astro.build/reference/icons/#all-icons
25
- */
26
- icon: z.string().optional(),
27
- /**
28
- * The topic label visible at the top of the sidebar.
29
- *
30
- * The value can be a string, or for multilingual sites, an object with values for each different locale. When using
31
- * the object form, the keys must be BCP-47 tags (e.g. en, fr, or zh-CN).
32
- */
33
- label: z.union([z.string(), z.record(z.string())]),
34
- /**
35
- * The link to the topic’s content which an be a relative link to local files or the full URL of an external page.
36
- *
37
- * For internal links, the link can either be a page included in the items array or a different page acting as the
38
- * topic’s landing page.
39
- */
40
- link: z.string(),
12
+ /**
13
+ * An optional badge to display next to the topic label.
14
+ *
15
+ * This option accepts the same configuration as the Starlight badge sidebar item configuration.
16
+ * @see https://starlight.astro.build/guides/sidebar/#badges
17
+ */
18
+ badge: z
19
+ .union([z.string(), sidebarTopicBadgeSchema])
20
+ .transform((badge) =>
21
+ typeof badge === "string"
22
+ ? { variant: "default" as const, text: badge }
23
+ : badge
24
+ )
25
+ .optional(),
26
+ /**
27
+ * The name of an optional icon to display before the topic label set to one of Starlight’s built-in icons.
28
+ * @see https://starlight.astro.build/reference/icons/#all-icons
29
+ */
30
+ icon: z.string().optional(),
31
+ /**
32
+ * The topic label visible at the top of the sidebar.
33
+ *
34
+ * The value can be a string, or for multilingual sites, an object with values for each different locale. When using
35
+ * the object form, the keys must be BCP-47 tags (e.g. en, fr, or zh-CN).
36
+ */
37
+ label: z.union([z.string(), z.record(z.string())]),
38
+ /**
39
+ * The link to the topic’s content which an be a relative link to local files or the full URL of an external page.
40
+ *
41
+ * For internal links, the link can either be a page included in the items array or a different page acting as the
42
+ * topic’s landing page.
43
+ */
44
+ link: z.string(),
41
45
  });
42
46
 
43
47
  const sidebarTopicLinkSchema = sidebarTopicBaseSchema;
44
48
 
45
49
  const sidebarTopicGroupSchema = sidebarTopicBaseSchema.extend({
46
- /**
47
- * An optional unique identifier for the topic that can be used to associate content pages that are not listed in any
48
- * topic sidebar configuration with this topic.
49
- */
50
- id: z.string().optional(),
51
- /**
52
- * The sidebar items (links and subcategories) to display for this topic.
53
- *
54
- * The topic’s sidebar navigation items. This represents the sidebar displayed when the topic `link` page or any of
55
- * the pages configured in the `items` array is the current page.
56
- */
57
- items: z.any().array() as z.Schema<NonNullable<StarlightUserConfig["sidebar"]>>,
50
+ /**
51
+ * An optional unique identifier for the topic that can be used to associate content pages that are not listed in any
52
+ * topic sidebar configuration with this topic.
53
+ */
54
+ id: z.string().optional(),
55
+ /**
56
+ * The sidebar items (links and subcategories) to display for this topic.
57
+ *
58
+ * The topic’s sidebar navigation items. This represents the sidebar displayed when the topic `link` page or any of
59
+ * the pages configured in the `items` array is the current page.
60
+ */
61
+ items: z.any().array() as z.Schema<
62
+ NonNullable<StarlightUserConfig["sidebar"]>
63
+ >,
58
64
  });
59
65
 
60
66
  export const StarlightSidebarTopicsDropdownConfigSchema = z
61
- .union([sidebarTopicGroupSchema, sidebarTopicLinkSchema])
62
- .array();
67
+ .union([sidebarTopicGroupSchema, sidebarTopicLinkSchema])
68
+ .array();
63
69
 
64
70
  export type StarlightSidebarTopicsDropdownUserConfig = z.input<
65
- typeof StarlightSidebarTopicsDropdownConfigSchema
71
+ typeof StarlightSidebarTopicsDropdownConfigSchema
66
72
  >;
67
73
  export type StarlightSidebarTopicsDropdownConfig = z.output<
68
- typeof StarlightSidebarTopicsDropdownConfigSchema
74
+ typeof StarlightSidebarTopicsDropdownConfigSchema
69
75
  >;
70
76
 
71
77
  export type StarlightSidebarTopicsDropdownSharedConfig = (
72
- | (z.output<typeof sidebarTopicLinkSchema> & { type: "link" })
73
- | (Omit<z.output<typeof sidebarTopicGroupSchema>, "items"> & { type: "group" })
78
+ | (z.output<typeof sidebarTopicLinkSchema> & { type: "link" })
79
+ | (Omit<z.output<typeof sidebarTopicGroupSchema>, "items"> & {
80
+ type: "group";
81
+ })
74
82
  )[];
package/lib/content.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import type { Props } from "@astrojs/starlight/props";
2
2
  import type { TopicFrontmatterSchema } from "../schema";
3
- export function isStarlightEntryWithTopic(entry: StarlightEntry): entry is StarlightEntryWithTopic {
4
- return (
5
- "data" in entry &&
6
- "topic" in (entry as StarlightEntryWithTopic).data &&
7
- typeof (entry as StarlightEntryWithTopic).data.topic === "string"
8
- );
3
+ export function isStarlightEntryWithTopic(
4
+ entry: StarlightEntry
5
+ ): entry is StarlightEntryWithTopic {
6
+ return (
7
+ "data" in entry &&
8
+ "topic" in (entry as StarlightEntryWithTopic).data &&
9
+ typeof (entry as StarlightEntryWithTopic).data.topic === "string"
10
+ );
9
11
  }
10
12
  export type StarlightEntry = Props["entry"];
11
- export type StarlightEntryWithTopic = StarlightEntry & { data: TopicFrontmatterSchema };
13
+ export type StarlightEntryWithTopic = StarlightEntry & {
14
+ data: TopicFrontmatterSchema;
15
+ };
package/lib/i18n.ts CHANGED
@@ -2,21 +2,26 @@ import starlightConfig from "virtual:starlight/user-config";
2
2
 
3
3
  import { stripTrailingSlash } from "./pathname";
4
4
 
5
- export function getLocalizedSlug(slug: string, locale: string | undefined): string {
6
- const slugLocale = getLocaleFromSlug(slug);
7
- if (slugLocale === locale) return slug;
8
- locale ??= "";
9
- if (slugLocale === slug) return locale;
5
+ export function getLocalizedSlug(
6
+ slug: string,
7
+ locale: string | undefined
8
+ ): string {
9
+ const slugLocale = getLocaleFromSlug(slug);
10
+ if (slugLocale === locale) return slug;
11
+ locale ??= "";
12
+ if (slugLocale === slug) return locale;
10
13
 
11
- if (slugLocale) {
12
- return stripTrailingSlash(slug.replace(`${slugLocale}/`, locale ? `${locale}/` : ""));
13
- }
14
+ if (slugLocale) {
15
+ return stripTrailingSlash(
16
+ slug.replace(`${slugLocale}/`, locale ? `${locale}/` : "")
17
+ );
18
+ }
14
19
 
15
- return slug ? `${locale}/${slug}` : locale;
20
+ return slug ? `${locale}/${slug}` : locale;
16
21
  }
17
22
 
18
23
  export function getLocaleFromSlug(slug: string): string | undefined {
19
- const locales = Object.keys(starlightConfig.locales ?? {});
20
- const baseSegment = slug.split("/")[0];
21
- return baseSegment && locales.includes(baseSegment) ? baseSegment : undefined;
24
+ const locales = Object.keys(starlightConfig.locales ?? {});
25
+ const baseSegment = slug.split("/")[0];
26
+ return baseSegment && locales.includes(baseSegment) ? baseSegment : undefined;
22
27
  }
package/lib/pathname.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  export function arePathnamesEqual(pathnameA: string, pathnameB: string) {
2
- return stripLeadingAndTrailingSlashes(pathnameA) === stripLeadingAndTrailingSlashes(pathnameB);
2
+ return (
3
+ stripLeadingAndTrailingSlashes(pathnameA) ===
4
+ stripLeadingAndTrailingSlashes(pathnameB)
5
+ );
3
6
  }
4
7
 
5
8
  export function stripLeadingAndTrailingSlashes(pathname: string): string {
6
- return stripLeadingSlash(stripTrailingSlash(pathname));
9
+ return stripLeadingSlash(stripTrailingSlash(pathname));
7
10
  }
8
11
 
9
12
  export function stripLeadingSlash(pathname: string) {
10
- if (pathname.startsWith("/")) pathname = pathname.slice(1);
11
- return pathname;
13
+ if (pathname.startsWith("/")) pathname = pathname.slice(1);
14
+ return pathname;
12
15
  }
13
16
 
14
17
  export function stripTrailingSlash(pathname: string) {
15
- if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
16
- return pathname;
18
+ if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
19
+ return pathname;
17
20
  }
package/lib/plugin.ts CHANGED
@@ -3,30 +3,30 @@ import type { AstroIntegrationLogger } from "astro";
3
3
  import { AstroError } from "astro/errors";
4
4
 
5
5
  export function throwPluginError(message: string, hint?: string): never {
6
- throw new AstroError(
7
- message,
8
- hint ??
9
- `See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/HiDeoo/starlight-sidebar-topics-dropdown/issues/new/choose`
10
- );
6
+ throw new AstroError(
7
+ message,
8
+ hint ??
9
+ `See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/HiDeoo/starlight-sidebar-topics-dropdown/issues/new/choose`
10
+ );
11
11
  }
12
12
 
13
13
  export function overrideStarlightComponent(
14
- components: StarlightUserConfig["components"],
15
- logger: AstroIntegrationLogger,
16
- component: keyof NonNullable<StarlightUserConfig["components"]>
14
+ components: StarlightUserConfig["components"],
15
+ logger: AstroIntegrationLogger,
16
+ component: keyof NonNullable<StarlightUserConfig["components"]>
17
17
  ) {
18
- if (components?.[component]) {
19
- logger.warn(
20
- `It looks like you already have a \`${component}\` component override in your Starlight configuration.`
21
- );
22
- logger.warn(
23
- `To use \`starlight-sidebar-topics-dropdown\`, either remove your override or update it to render the content from \`starlight-sidebar-topics-dropdown/overrides/${component}.astro\`.`
24
- );
18
+ if (components?.[component]) {
19
+ logger.warn(
20
+ `It looks like you already have a \`${component}\` component override in your Starlight configuration.`
21
+ );
22
+ logger.warn(
23
+ `To use \`starlight-sidebar-topics-dropdown\`, either remove your override or update it to render the content from \`starlight-sidebar-topics-dropdown/overrides/${component}.astro\`.`
24
+ );
25
25
 
26
- return {};
27
- }
26
+ return {};
27
+ }
28
28
 
29
- return {
30
- [component]: `starlight-sidebar-topics-dropdown/overrides/${component}.astro`,
31
- };
29
+ return {
30
+ [component]: `starlight-sidebar-topics-dropdown/overrides/${component}.astro`,
31
+ };
32
32
  }
package/lib/sidebar.ts CHANGED
@@ -8,166 +8,203 @@ import { arePathnamesEqual, stripLeadingAndTrailingSlashes } from "./pathname";
8
8
  const absoluteLinkRegex = /^https?:\/\//;
9
9
 
10
10
  export function getCurrentTopic(
11
- config: StarlightSidebarTopicsDropdownSharedConfig,
12
- sidebar: SidebarEntry[],
13
- currentSlug: string,
14
- entry: StarlightEntry
11
+ config: StarlightSidebarTopicsDropdownSharedConfig,
12
+ sidebar: SidebarEntry[],
13
+ currentSlug: string,
14
+ entry: StarlightEntry
15
15
  ): Topic | undefined {
16
- // If the current page has a topic ID, use it to find the topic.
17
- const topicId = getTopicIdFromEntry(entry);
18
- if (topicId) return getTopicById(config, sidebar, topicId);
19
-
20
- // Start by checking if the current page is a topic root.
21
- const topicFromSlug = getTopicFromSlug(config, sidebar, currentSlug);
22
- if (topicFromSlug) return topicFromSlug;
23
-
24
- // Otherwise, find the current topic by looking for the current page in the sidebar.
25
- const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
26
- if (!currentSidebarTopic) return;
27
-
28
- const currentTopicConfig = config[Number.parseInt(currentSidebarTopic.label, 10)];
29
- if (!currentTopicConfig) return;
30
-
31
- return { config: currentTopicConfig, sidebar: currentSidebarTopic.entries };
16
+ // If the current page has a topic ID, use it to find the topic.
17
+ const topicId = getTopicIdFromEntry(entry);
18
+ if (topicId) return getTopicById(config, sidebar, topicId);
19
+
20
+ // Start by checking if the current page is a topic root.
21
+ const topicFromSlug = getTopicFromSlug(config, sidebar, currentSlug);
22
+ if (topicFromSlug) return topicFromSlug;
23
+
24
+ // Otherwise, find the current topic by looking for the current page in the sidebar.
25
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
26
+ if (!currentSidebarTopic) return;
27
+
28
+ const currentTopicConfig =
29
+ config[Number.parseInt(currentSidebarTopic.label, 10)];
30
+ if (!currentTopicConfig) return;
31
+
32
+ const sidebarHasGroups =
33
+ currentSidebarTopic.entries.length !== 1 ||
34
+ currentSidebarTopic.entries[0]?.label !== "";
35
+
36
+ return {
37
+ config: currentTopicConfig,
38
+ sidebar: currentSidebarTopic.entries,
39
+ sidebarHasGroups,
40
+ };
32
41
  }
33
42
 
34
- export function isTopicFirstPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
35
- const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
36
- if (!currentSidebarTopic) return false;
43
+ export function isTopicFirstPage(
44
+ sidebar: SidebarEntry[],
45
+ currentSlug: string
46
+ ): boolean {
47
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
48
+ if (!currentSidebarTopic) return false;
37
49
 
38
- const firstPage = getSidebarFirstPage(currentSidebarTopic.entries);
39
- if (!firstPage) return false;
50
+ const firstPage = getSidebarFirstPage(currentSidebarTopic.entries);
51
+ if (!firstPage) return false;
40
52
 
41
- return arePathnamesEqual(stripLeadingAndTrailingSlashes(firstPage.href), currentSlug);
53
+ return arePathnamesEqual(
54
+ stripLeadingAndTrailingSlashes(firstPage.href),
55
+ currentSlug
56
+ );
42
57
  }
43
58
 
44
- export function isTopicLastPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
45
- const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
46
- if (!currentSidebarTopic) return false;
59
+ export function isTopicLastPage(
60
+ sidebar: SidebarEntry[],
61
+ currentSlug: string
62
+ ): boolean {
63
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
64
+ if (!currentSidebarTopic) return false;
47
65
 
48
- const lastPage = getSidebarLastPage(currentSidebarTopic.entries);
49
- if (!lastPage) return false;
66
+ const lastPage = getSidebarLastPage(currentSidebarTopic.entries);
67
+ if (!lastPage) return false;
50
68
 
51
- return arePathnamesEqual(stripLeadingAndTrailingSlashes(lastPage.href), currentSlug);
69
+ return arePathnamesEqual(
70
+ stripLeadingAndTrailingSlashes(lastPage.href),
71
+ currentSlug
72
+ );
52
73
  }
53
74
 
54
75
  function getSidebarFirstPage(sidebar: SidebarEntry[]) {
55
- const entry = sidebar[0];
56
- if (!entry) return;
57
- if (entry.type === "link") return entry;
76
+ const entry = sidebar[0];
77
+ if (!entry) return;
78
+ if (entry.type === "link") return entry;
58
79
 
59
- return getSidebarFirstPage(entry.entries);
80
+ return getSidebarFirstPage(entry.entries);
60
81
  }
61
82
 
62
83
  function getSidebarLastPage(sidebar: SidebarEntry[]) {
63
- const entry = sidebar.at(-1);
64
- if (!entry) return;
65
- if (entry.type === "link") return entry;
84
+ const entry = sidebar.at(-1);
85
+ if (!entry) return;
86
+ if (entry.type === "link") return entry;
66
87
 
67
- return getSidebarLastPage(entry.entries);
88
+ return getSidebarLastPage(entry.entries);
68
89
  }
69
90
 
70
91
  function getTopicFromSlug(
71
- config: StarlightSidebarTopicsDropdownSharedConfig,
72
- sidebar: SidebarEntry[],
73
- slug: string
92
+ config: StarlightSidebarTopicsDropdownSharedConfig,
93
+ sidebar: SidebarEntry[],
94
+ slug: string
74
95
  ): Topic | undefined {
75
- let topicConfig: Topic["config"] | undefined;
76
- let topicSidebar: Topic["sidebar"] | undefined;
77
-
78
- // Start by checking if the current page is a topic homepage.
79
- let groupTopicIndex = -1;
80
- const slugLocale = getLocaleFromSlug(slug);
81
-
82
- for (const topic of config) {
83
- if (topic.type === "group") groupTopicIndex++;
84
-
85
- if (
86
- !absoluteLinkRegex.test(topic.link) &&
87
- arePathnamesEqual(
88
- getLocalizedSlug(stripLeadingAndTrailingSlashes(topic.link), slugLocale),
89
- slug
90
- ) &&
91
- groupTopicIndex !== -1
92
- ) {
93
- const sidebarTopic = sidebar[groupTopicIndex];
94
-
95
- if (sidebarTopic?.type === "group") {
96
- topicConfig = topic;
97
- topicSidebar = sidebarTopic.entries;
98
- }
99
-
100
- break;
101
- }
96
+ let topicConfig: Topic["config"] | undefined;
97
+ let topicSidebar: Topic["sidebar"] | undefined;
98
+ let sidebarHasGroups = true;
99
+
100
+ // Start by checking if the current page is a topic homepage.
101
+ let groupTopicIndex = -1;
102
+ const slugLocale = getLocaleFromSlug(slug);
103
+
104
+ for (const topic of config) {
105
+ if (topic.type === "group") groupTopicIndex++;
106
+
107
+ if (
108
+ !absoluteLinkRegex.test(topic.link) &&
109
+ arePathnamesEqual(
110
+ getLocalizedSlug(
111
+ stripLeadingAndTrailingSlashes(topic.link),
112
+ slugLocale
113
+ ),
114
+ slug
115
+ ) &&
116
+ groupTopicIndex !== -1
117
+ ) {
118
+ const sidebarTopic = sidebar[groupTopicIndex];
119
+
120
+ if (sidebarTopic?.type === "group") {
121
+ topicConfig = topic;
122
+ topicSidebar = sidebarTopic.entries;
123
+ sidebarHasGroups =
124
+ sidebarTopic.entries.length !== 1 ||
125
+ sidebarTopic.entries[0]?.label !== "";
126
+ }
127
+
128
+ break;
102
129
  }
130
+ }
103
131
 
104
- if (!topicConfig || !topicSidebar) return;
132
+ if (!topicConfig || !topicSidebar) return;
105
133
 
106
- return { config: topicConfig, sidebar: topicSidebar };
134
+ return { config: topicConfig, sidebar: topicSidebar, sidebarHasGroups };
107
135
  }
108
136
 
109
137
  function getTopicById(
110
- config: StarlightSidebarTopicsDropdownSharedConfig,
111
- sidebar: SidebarEntry[],
112
- id: string
138
+ config: StarlightSidebarTopicsDropdownSharedConfig,
139
+ sidebar: SidebarEntry[],
140
+ id: string
113
141
  ): Topic | undefined {
114
- let topicConfig: Topic["config"] | undefined;
115
- let topicSidebar: Topic["sidebar"] | undefined;
116
- let groupTopicIndex = -1;
117
- for (const topic of config) {
118
- if (topic.type === "group") groupTopicIndex++;
119
- if (topic.type === "group" && topic.id === id) {
120
- const sidebarTopic = sidebar[groupTopicIndex];
121
- if (sidebarTopic?.type === "group") {
122
- topicConfig = topic;
123
- topicSidebar = sidebarTopic.entries;
124
- }
125
- break;
126
- }
142
+ let topicConfig: Topic["config"] | undefined;
143
+ let topicSidebar: Topic["sidebar"] | undefined;
144
+ let sidebarHasGroups = true;
145
+ let groupTopicIndex = -1;
146
+ for (const topic of config) {
147
+ if (topic.type === "group") groupTopicIndex++;
148
+ if (topic.type === "group" && topic.id === id) {
149
+ const sidebarTopic = sidebar[groupTopicIndex];
150
+ if (sidebarTopic?.type === "group") {
151
+ topicConfig = topic;
152
+ topicSidebar = sidebarTopic.entries;
153
+ sidebarHasGroups =
154
+ sidebarTopic.entries.length !== 1 ||
155
+ sidebarTopic.entries[0]?.label !== "";
156
+ }
157
+ break;
127
158
  }
128
- if (!topicConfig || !topicSidebar) return;
129
- return { config: topicConfig, sidebar: topicSidebar };
159
+ }
160
+ if (!topicConfig || !topicSidebar) return;
161
+ return { config: topicConfig, sidebar: topicSidebar, sidebarHasGroups };
130
162
  }
131
163
 
132
- function getCurrentSidebarTopic(sidebar: SidebarEntry[]): SidebarTopic | undefined {
133
- let currentSidebarTopic: SidebarTopic | undefined;
164
+ function getCurrentSidebarTopic(
165
+ sidebar: SidebarEntry[]
166
+ ): SidebarTopic | undefined {
167
+ let currentSidebarTopic: SidebarTopic | undefined;
134
168
 
135
- for (const topic of sidebar) {
136
- if (topic.type === "link") continue;
169
+ for (const topic of sidebar) {
170
+ if (topic.type === "link") continue;
137
171
 
138
- const currentSidebarEntry = getCurrentSidebarEntry(topic.entries);
172
+ const currentSidebarEntry = getCurrentSidebarEntry(topic.entries);
139
173
 
140
- if (currentSidebarEntry) {
141
- currentSidebarTopic = topic;
142
- break;
143
- }
174
+ if (currentSidebarEntry) {
175
+ currentSidebarTopic = topic;
176
+ break;
144
177
  }
178
+ }
145
179
 
146
- return currentSidebarTopic;
180
+ return currentSidebarTopic;
147
181
  }
148
182
 
149
- function getCurrentSidebarEntry(sidebar: SidebarEntry[]): SidebarEntry | undefined {
150
- return sidebar.find((entry) => {
151
- if (entry.type === "link") {
152
- return entry.isCurrent;
153
- }
183
+ function getCurrentSidebarEntry(
184
+ sidebar: SidebarEntry[]
185
+ ): SidebarEntry | undefined {
186
+ return sidebar.find((entry) => {
187
+ if (entry.type === "link") {
188
+ return entry.isCurrent;
189
+ }
154
190
 
155
- return getCurrentSidebarEntry(entry.entries);
156
- });
191
+ return getCurrentSidebarEntry(entry.entries);
192
+ });
157
193
  }
158
194
 
159
195
  function getTopicIdFromEntry(entry: StarlightEntry): string | undefined {
160
- return isStarlightEntryWithTopic(entry) ? entry.data.topic : undefined;
196
+ return isStarlightEntryWithTopic(entry) ? entry.data.topic : undefined;
161
197
  }
162
198
 
163
199
  type SidebarEntry = Props["sidebar"][number];
164
200
 
165
201
  interface SidebarTopic {
166
- label: string;
167
- entries: SidebarEntry[];
202
+ label: string;
203
+ entries: SidebarEntry[];
168
204
  }
169
205
 
170
206
  export interface Topic {
171
- config: StarlightSidebarTopicsDropdownSharedConfig[number];
172
- sidebar: SidebarEntry[];
207
+ config: StarlightSidebarTopicsDropdownSharedConfig[number];
208
+ sidebar: SidebarEntry[];
209
+ sidebarHasGroups: boolean;
173
210
  }
package/lib/vite.ts CHANGED
@@ -1,34 +1,36 @@
1
1
  import type { ViteUserConfig } from "astro";
2
2
 
3
3
  import type {
4
- StarlightSidebarTopicsDropdownConfig,
5
- StarlightSidebarTopicsDropdownSharedConfig,
4
+ StarlightSidebarTopicsDropdownConfig,
5
+ StarlightSidebarTopicsDropdownSharedConfig,
6
6
  } from "./config";
7
7
 
8
8
  const moduleId = "virtual:starlight-sidebar-topics-dropdown/config";
9
9
 
10
10
  export function vitePluginStarlightSidebarTopicsDropdown(
11
- config: StarlightSidebarTopicsDropdownConfig
11
+ config: StarlightSidebarTopicsDropdownConfig
12
12
  ): VitePlugin {
13
- const resolvedModuleId = `\0${moduleId}`;
13
+ const resolvedModuleId = `\0${moduleId}`;
14
14
 
15
- const sharedConfig: StarlightSidebarTopicsDropdownSharedConfig = config.map((topic) => {
16
- if (!("items" in topic)) return { ...topic, type: "link" };
17
- const { items, ...topicWithoutItems } = topic;
18
- return { ...topicWithoutItems, type: "group" };
19
- });
15
+ const sharedConfig: StarlightSidebarTopicsDropdownSharedConfig = config.map(
16
+ (topic) => {
17
+ if (!("items" in topic)) return { ...topic, type: "link" };
18
+ const { items, ...topicWithoutItems } = topic;
19
+ return { ...topicWithoutItems, type: "group" };
20
+ }
21
+ );
20
22
 
21
- const moduleContent = `export default ${JSON.stringify(sharedConfig)}`;
23
+ const moduleContent = `export default ${JSON.stringify(sharedConfig)}`;
22
24
 
23
- return {
24
- name: "vite-plugin-starlight-sidebar-topics-dropdown",
25
- load(id) {
26
- return id === resolvedModuleId ? moduleContent : undefined;
27
- },
28
- resolveId(id) {
29
- return id === moduleId ? resolvedModuleId : undefined;
30
- },
31
- };
25
+ return {
26
+ name: "vite-plugin-starlight-sidebar-topics-dropdown",
27
+ load(id) {
28
+ return id === resolvedModuleId ? moduleContent : undefined;
29
+ },
30
+ resolveId(id) {
31
+ return id === moduleId ? resolvedModuleId : undefined;
32
+ },
33
+ };
32
34
  }
33
35
 
34
36
  type VitePlugin = NonNullable<ViteUserConfig["plugins"]>[number];
@@ -4,14 +4,14 @@ import type { Props } from '@astrojs/starlight/props'
4
4
 
5
5
  import { isTopicFirstPage, isTopicLastPage } from '../lib/sidebar'
6
6
 
7
- const { pagination, sidebar, slug } = Astro.props
7
+ const { id, pagination, sidebar } = Astro.props
8
8
  let { prev, next } = pagination
9
9
 
10
- if (isTopicFirstPage(sidebar, slug)) {
10
+ if (isTopicFirstPage(sidebar, id)) {
11
11
  prev = undefined
12
12
  }
13
13
 
14
- if (isTopicLastPage(sidebar, slug)) {
14
+ if (isTopicLastPage(sidebar, id)) {
15
15
  next = undefined
16
16
  }
17
17
  ---
@@ -7,16 +7,16 @@ import Topics from '../components/Topics.astro'
7
7
  import { throwPluginError } from '../lib/plugin'
8
8
  import { getCurrentTopic, type Topic } from '../lib/sidebar'
9
9
 
10
- const { entry, sidebar, slug } = Astro.props
10
+ const { id, entry, sidebar } = Astro.props
11
11
 
12
12
  let currentSidebar = sidebar
13
13
  let currentTopic: Topic | undefined = undefined
14
14
 
15
15
  if (entry['data'].template !== 'splash') {
16
- currentTopic = getCurrentTopic(config, sidebar, slug, entry)
16
+ currentTopic = getCurrentTopic(config, sidebar, id, entry)
17
17
  if (!currentTopic)
18
18
  throwPluginError(
19
- `Failed to find the topic for the \`${slug}\` page.`,
19
+ `Failed to find the topic for the \`${id}\` page.`,
20
20
  `Either include this page in the sidebar configuration of the desired topic using the \`items\` property or to associate an unlisted page with a topic, use the \`topic\` frontmatter property and set it to the desired topic ID.
21
21
  Learn more about unlisted pages in the ["Unlisted pages"](https://starlight-sidebar-topics.netlify.app/docs/guides/unlisted-pages/) guide.`,
22
22
  )
@@ -25,4 +25,16 @@ Learn more about unlisted pages in the ["Unlisted pages"](https://starlight-side
25
25
  ---
26
26
 
27
27
  {entry['data'].template !== 'splash' && currentTopic && <Topics current={currentTopic.config} />}
28
- <Default {...Astro.props} sidebar={currentSidebar}><slot /></Default>
28
+ <Default {...Astro.props} sidebar={currentSidebar}><slot /></Default>
29
+
30
+ {!currentTopic?.sidebarHasGroups &&
31
+ <style is:global>
32
+ sl-sidebar-state-persist summary {
33
+ display: none !important;
34
+ }
35
+ sl-sidebar-state-persist ul li {
36
+ margin-inline-start: 0;
37
+ border-inline-start: none;
38
+ }
39
+ </style>
40
+ }
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
1
  {
2
- "name": "starlight-sidebar-topics-dropdown",
3
- "version": "0.2.2",
4
- "license": "MIT",
5
- "description": "Starlight plugin to split your docs page into multiple subpages and switch between them with a dropdown menu in the sidebar.",
6
- "author": "trueberryless (https://trueberryless.org)",
7
- "type": "module",
8
- "exports": {
9
- ".": "./index.ts",
10
- "./overrides/Pagination.astro": "./overrides/Pagination.astro",
11
- "./overrides/Sidebar.astro": "./overrides/Sidebar.astro",
12
- "./schema": "./schema.ts",
13
- "./package.json": "./package.json"
14
- },
15
- "devDependencies": {
16
- "@astrojs/starlight": "^0.28.3",
17
- "astro": "^4.16.6"
18
- },
19
- "peerDependencies": {
20
- "@astrojs/starlight": ">=0.28"
21
- },
22
- "engines": {
23
- "node": "^18.17.1 || ^20.3.0 || >=21.0.0"
24
- },
25
- "packageManager": "pnpm@9.3.0",
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "keywords": [
30
- "starlight",
31
- "plugin",
32
- "sidebar",
33
- "subpages",
34
- "sections",
35
- "dropdown",
36
- "documentation",
37
- "astro"
38
- ],
39
- "homepage": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown",
40
- "repository": {
41
- "type": "git",
42
- "url": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown.git",
43
- "directory": "packages/starlight-sidebar-topics-dropdown"
44
- },
45
- "bugs": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/issues"
2
+ "name": "starlight-sidebar-topics-dropdown",
3
+ "version": "0.3.0",
4
+ "description": "Starlight plugin to split your docs page into multiple subpages and switch between them with a dropdown menu in the sidebar.",
5
+ "keywords": [
6
+ "starlight",
7
+ "plugin",
8
+ "sidebar",
9
+ "subpages",
10
+ "sections",
11
+ "dropdown",
12
+ "documentation",
13
+ "astro"
14
+ ],
15
+ "homepage": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown",
16
+ "bugs": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/issues",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown.git",
20
+ "directory": "packages/starlight-sidebar-topics-dropdown"
21
+ },
22
+ "license": "MIT",
23
+ "author": "trueberryless (https://trueberryless.org)",
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./index.ts",
27
+ "./overrides/Pagination.astro": "./overrides/Pagination.astro",
28
+ "./overrides/Sidebar.astro": "./overrides/Sidebar.astro",
29
+ "./schema": "./schema.ts",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "devDependencies": {
33
+ "@astrojs/starlight": "^0.30.3",
34
+ "astro": "^5.1.1"
35
+ },
36
+ "peerDependencies": {
37
+ "@astrojs/starlight": ">=0.30"
38
+ },
39
+ "packageManager": "pnpm@9.3.0",
40
+ "engines": {
41
+ "node": "^18.17.1 || ^20.3.0 || >=21.0.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
46
  }
package/schema.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { z } from "astro/zod";
2
2
  export const topicSchema = z.object({
3
- /**
4
- * ID of the topic to associate with the current page if the page is not listed in any topic sidebar configuration.
5
- *
6
- * @see https://starlight-sidebar-topics.netlify.app/docs/guides/unlisted-pages/
7
- */
8
- topic: z.string().optional(),
3
+ /**
4
+ * ID of the topic to associate with the current page if the page is not listed in any topic sidebar configuration.
5
+ *
6
+ * @see https://starlight-sidebar-topics.netlify.app/docs/guides/unlisted-pages/
7
+ */
8
+ topic: z.string().optional(),
9
9
  });
10
10
  export type TopicFrontmatterSchema = z.input<typeof topicSchema>;
package/starlight.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  declare module "virtual:starlight/user-config" {
2
- const Config: import("@astrojs/starlight/types").StarlightConfig;
2
+ const Config: import("@astrojs/starlight/types").StarlightConfig;
3
3
 
4
- export default Config;
4
+ export default Config;
5
5
  }
package/virtual.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  declare module "virtual:starlight-sidebar-topics-dropdown/config" {
2
- const StarlightSidebarTopicsDropdownConfig: import("./libs/config").StarlightSidebarTopicsDropdownSharedConfig;
2
+ const StarlightSidebarTopicsDropdownConfig: import("./libs/config").StarlightSidebarTopicsDropdownSharedConfig;
3
3
 
4
- export default StarlightSidebarTopicsDropdownConfig;
4
+ export default StarlightSidebarTopicsDropdownConfig;
5
5
  }