This commit is contained in:
babayaga 2025-08-21 12:34:46 +02:00
parent e9570bde94
commit 7a8e2606ba
9 changed files with 610 additions and 88 deletions

View File

@ -0,0 +1,467 @@
import type { CollectionEntry } from 'astro:content';
import { isFolder } from '@polymech/commons';
import { parseFrontmatter } from '@astrojs/markdown-remark';
// Filter function type
export type CollectionFilter<T = any> = (entry: CollectionEntry<T>, astroConfig?: any) => boolean;
// Config interface for collection filters
export interface CollectionFilterConfig {
ENABLE_VALID_FRONTMATTER_CHECK?: boolean;
ENABLE_FOLDER_FILTER?: boolean;
ENABLE_DRAFT_FILTER?: boolean;
ENABLE_TITLE_FILTER?: boolean;
ENABLE_BODY_FILTER?: boolean;
ENABLE_DESCRIPTION_FILTER?: boolean;
ENABLE_IMAGE_FILTER?: boolean;
ENABLE_AUTHOR_FILTER?: boolean;
ENABLE_PUBDATE_FILTER?: boolean;
ENABLE_TAGS_FILTER?: boolean;
ENABLE_FILE_EXTENSION_FILTER?: boolean;
REQUIRED_FIELDS?: string[];
REQUIRED_TAGS?: string[];
EXCLUDE_TAGS?: string[];
FILTER_FUTURE_POSTS?: boolean;
FILTER_OLD_POSTS?: boolean;
OLD_POST_CUTOFF_DAYS?: number;
}
// Default filters
export const hasValidFrontMatter: CollectionFilter = (entry) => {
// Check if the entry has valid frontmatter
// For MD/MDX files, Astro automatically parses frontmatter
// If data exists and is not empty, consider it valid
if (!entry.data) return false;
// Check for basic required fields (can be customized)
// At minimum, we expect some data to exist
return typeof entry.data === 'object' && Object.keys(entry.data).length > 0;
};
/**
* Advanced frontmatter validation using Astro's parseFrontmatter
* This can be used for more thorough validation of raw markdown content
* @param rawContent - Optional raw markdown content to parse
*/
export const hasValidParsedFrontMatter: CollectionFilter = (entry) => {
try {
// If entry already has parsed data, it's valid
if (entry.data && typeof entry.data === 'object' && Object.keys(entry.data).length > 0) {
return true;
}
// For entries that might need raw frontmatter parsing
// This is more applicable when working with raw markdown content
// In most Astro cases, entry.data is already parsed
return false;
} catch (error) {
console.warn(`Frontmatter parsing failed for entry ${entry.id}:`, error);
return false;
}
};
/**
* Create a filter that validates frontmatter against a schema or validation function
* @param validator - Function that validates the frontmatter data
* @returns Filter function
*/
export function createFrontmatterValidator<T = any>(
validator: (data: any) => boolean
): CollectionFilter<T> {
return (entry) => {
try {
if (!entry.data) return false;
return validator(entry.data);
} catch (error) {
console.warn(`Frontmatter validation failed for entry ${entry.id}:`, error);
return false;
}
};
}
/**
* Create a filter that parses and validates raw markdown content
* Useful for validating files that haven't been processed by Astro yet
* @param rawContentGetter - Function to get raw content for an entry
* @param validator - Optional validator function for the parsed frontmatter
* @returns Filter function
*/
export function createRawFrontmatterValidator<T = any>(
rawContentGetter: (entry: CollectionEntry<T>) => string,
validator?: (data: any) => boolean
): CollectionFilter<T> {
return (entry) => {
try {
const rawContent = rawContentGetter(entry);
if (!rawContent) return false;
const parsed = parseFrontmatter(rawContent);
// Check if frontmatter exists and is valid
if (!parsed.frontmatter || typeof parsed.frontmatter !== 'object') {
return false;
}
// Apply custom validator if provided
if (validator) {
return validator(parsed.frontmatter);
}
// Default validation: frontmatter should have at least one property
return Object.keys(parsed.frontmatter).length > 0;
} catch (error) {
console.warn(`Raw frontmatter parsing failed for entry ${entry.id}:`, error);
return false;
}
};
}
/**
* Create a filter that reads and validates frontmatter from the actual file
* Uses the entry's filePath to read the raw file content
* @param validator - Optional validator function for the parsed frontmatter
* @returns Filter function
*/
export function createFileBasedFrontmatterValidator<T = any>(
validator?: (data: any) => boolean
): CollectionFilter<T> {
return (entry) => {
try {
if (!entry.filePath) {
console.warn(`No filePath available for entry ${entry.id}`);
return false;
}
// This would require fs import in a Node.js environment
// For now, we'll rely on the entry.data which is already parsed
// In a real implementation, you could use:
// const fs = await import('fs');
// const rawContent = fs.readFileSync(entry.filePath, 'utf-8');
// const parsed = parseFrontmatter(rawContent);
// Fallback to using the already parsed data
if (!entry.data || typeof entry.data !== 'object') {
return false;
}
if (validator) {
return validator(entry.data);
}
return Object.keys(entry.data).length > 0;
} catch (error) {
console.warn(`File-based frontmatter validation failed for entry ${entry.id}:`, error);
return false;
}
};
}
export const isNotFolder: CollectionFilter = (entry) => {
// Check if the entry is not a folder
// Use filePath if available, otherwise construct from collection and id
const entryPath = entry.filePath || `src/content/${entry.collection}/${entry.id}`;
return !isFolder(entryPath);
};
export const isNotDraft: CollectionFilter = (entry) => {
// Filter out draft entries
return !entry.data?.draft;
};
export const hasTitle: CollectionFilter = (entry) => {
// Check if entry has a title and it's not the default "Untitled"
return !!entry.data?.title &&
entry.data.title.trim() !== '' &&
entry.data.title.trim() !== 'Untitled';
};
export const hasBody: CollectionFilter = (entry) => {
// Check if entry has body content
return !!entry.body && entry.body.trim() !== '';
};
export const hasValidFileExtension: CollectionFilter = (entry) => {
// Check if the entry has a valid markdown/mdx file extension
if (!entry.filePath) return true; // If no filePath, assume it's valid
const validExtensions = ['.md', '.mdx'];
return validExtensions.some(ext => entry.filePath.endsWith(ext));
};
export const hasImage: CollectionFilter = (entry) => {
// Check if entry has an image defined
return !!(entry.data?.image?.url);
};
export const hasDescription: CollectionFilter = (entry) => {
// Check if entry has a non-empty description
return !!entry.data?.description && entry.data.description.trim() !== '';
};
export const hasAuthor: CollectionFilter = (entry) => {
// Check if entry has an author (and it's not the default "Unknown")
return !!entry.data?.author &&
entry.data.author.trim() !== '' &&
entry.data.author !== 'Unknown';
};
export const hasPubDate: CollectionFilter = (entry) => {
// Check if entry has a valid publication date
if (!entry.data?.pubDate) return false;
try {
const date = new Date(entry.data.pubDate);
return !isNaN(date.getTime());
} catch {
return false;
}
};
export const hasTags: CollectionFilter = (entry) => {
// Check if entry has tags
return !!(entry.data?.tags && Array.isArray(entry.data.tags) && entry.data.tags.length > 0);
};
// Default filters array
export const defaultFilters: CollectionFilter[] = [
hasValidFrontMatter,
isNotFolder,
isNotDraft,
hasTitle // Include title validation by default to filter out "Untitled" entries
];
/**
* Generic filter function for collections
* @param collection - Array of collection entries from getCollection()
* @param filters - Array of filter functions to apply (defaults to defaultFilters)
* @param astroConfig - Optional Astro config object
* @returns Filtered array of collection entries
*/
export function filterCollection<T = any>(
collection: CollectionEntry<T>[],
filters: CollectionFilter<T>[] = defaultFilters,
astroConfig?: any
): CollectionEntry<T>[] {
return collection.filter(entry => {
// Apply all filters - entry must pass ALL filters to be included
return filters.every(filter => {
try {
return filter(entry, astroConfig);
} catch (error) {
console.warn(`Filter failed for entry ${entry.id}:`, error);
return false; // If filter throws, exclude the entry
}
});
});
}
/**
* Convenience function to create custom filter combinations
* @param baseFilters - Base filters to start with
* @param additionalFilters - Additional filters to add
* @returns Combined filter array
*/
export function combineFilters<T = any>(
baseFilters: CollectionFilter<T>[] = defaultFilters,
additionalFilters: CollectionFilter<T>[] = []
): CollectionFilter<T>[] {
return [...baseFilters, ...additionalFilters];
}
/**
* Create a filter that checks for specific frontmatter fields
* @param requiredFields - Array of field names that must exist
* @returns Filter function
*/
export function createRequiredFieldsFilter<T = any>(requiredFields: string[]): CollectionFilter<T> {
return (entry) => {
if (!entry.data) return false;
return requiredFields.every(field =>
entry.data[field] !== undefined && entry.data[field] !== null && entry.data[field] !== ''
);
};
}
/**
* Create a filter that checks for specific tags
* @param requiredTags - Array of tags that must be present
* @param matchAll - If true, all tags must be present; if false, at least one tag must be present
* @returns Filter function
*/
export function createTagFilter<T = any>(requiredTags: string[], matchAll: boolean = false): CollectionFilter<T> {
return (entry) => {
const entryTags = entry.data?.tags || [];
if (!Array.isArray(entryTags)) return false;
if (matchAll) {
return requiredTags.every(tag => entryTags.includes(tag));
} else {
return requiredTags.some(tag => entryTags.includes(tag));
}
};
}
/**
* Create a filter based on publication date
* @param beforeDate - Optional date - entries must be published before this date
* @param afterDate - Optional date - entries must be published after this date
* @returns Filter function
*/
export function createDateFilter<T = any>(beforeDate?: Date, afterDate?: Date): CollectionFilter<T> {
return (entry) => {
const pubDate = entry.data?.pubDate;
if (!pubDate) return false;
const entryDate = new Date(pubDate);
if (beforeDate && entryDate > beforeDate) return false;
if (afterDate && entryDate < afterDate) return false;
return true;
};
}
/**
* Create a filter that excludes entries with specific tags
* @param excludeTags - Array of tags to exclude
* @returns Filter function
*/
export function createExcludeTagsFilter<T = any>(excludeTags: string[]): CollectionFilter<T> {
return (entry) => {
const entryTags = entry.data?.tags || [];
if (!Array.isArray(entryTags)) return true;
return !excludeTags.some(tag => entryTags.includes(tag));
};
}
/**
* Filter that excludes future posts
*/
export const isNotFuture: CollectionFilter = (entry) => {
const pubDate = entry.data?.pubDate;
if (!pubDate) return true; // If no date, include it
const entryDate = new Date(pubDate);
const now = new Date();
return entryDate <= now;
};
/**
* Create a filter that excludes old posts based on cutoff days
* @param cutoffDays - Number of days to consider a post "old"
* @returns Filter function
*/
export function createOldPostFilter<T = any>(cutoffDays: number): CollectionFilter<T> {
return (entry) => {
const pubDate = entry.data?.pubDate;
if (!pubDate) return true; // If no date, include it
const entryDate = new Date(pubDate);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - cutoffDays);
return entryDate > cutoffDate;
};
}
/**
* Build filters array based on configuration
* @param config - Collection filter configuration
* @returns Array of filter functions
*/
export function buildFiltersFromConfig<T = any>(config: CollectionFilterConfig): CollectionFilter<T>[] {
const filters: CollectionFilter<T>[] = [];
// Add default filters based on config
if (config.ENABLE_VALID_FRONTMATTER_CHECK !== false) {
filters.push(hasValidFrontMatter);
}
if (config.ENABLE_FOLDER_FILTER !== false) {
filters.push(isNotFolder);
}
if (config.ENABLE_DRAFT_FILTER !== false) {
filters.push(isNotDraft);
}
if (config.ENABLE_TITLE_FILTER) {
filters.push(hasTitle);
}
// Add content validation filters
if (config.ENABLE_BODY_FILTER) {
filters.push(hasBody);
}
if (config.ENABLE_DESCRIPTION_FILTER) {
filters.push(hasDescription);
}
if (config.ENABLE_IMAGE_FILTER) {
filters.push(hasImage);
}
if (config.ENABLE_AUTHOR_FILTER) {
filters.push(hasAuthor);
}
if (config.ENABLE_PUBDATE_FILTER) {
filters.push(hasPubDate);
}
if (config.ENABLE_TAGS_FILTER) {
filters.push(hasTags);
}
if (config.ENABLE_FILE_EXTENSION_FILTER !== false) {
filters.push(hasValidFileExtension);
}
// Add required fields filter
if (config.REQUIRED_FIELDS && config.REQUIRED_FIELDS.length > 0) {
filters.push(createRequiredFieldsFilter(config.REQUIRED_FIELDS));
}
// Add required tags filter
if (config.REQUIRED_TAGS && config.REQUIRED_TAGS.length > 0) {
filters.push(createTagFilter(config.REQUIRED_TAGS, true)); // matchAll = true
}
// Add exclude tags filter
if (config.EXCLUDE_TAGS && config.EXCLUDE_TAGS.length > 0) {
filters.push(createExcludeTagsFilter(config.EXCLUDE_TAGS));
}
// Add future posts filter
if (config.FILTER_FUTURE_POSTS) {
filters.push(isNotFuture);
}
// Add old posts filter
if (config.FILTER_OLD_POSTS && config.OLD_POST_CUTOFF_DAYS) {
filters.push(createOldPostFilter(config.OLD_POST_CUTOFF_DAYS));
}
return filters;
}
/**
* Convenience function that applies filters based on config
* @param collection - Array of collection entries
* @param config - Collection filter configuration
* @param astroConfig - Optional Astro config
* @returns Filtered collection entries
*/
export function filterCollectionWithConfig<T = any>(
collection: CollectionEntry<T>[],
config: CollectionFilterConfig,
astroConfig?: any
): CollectionEntry<T>[] {
const filters = buildFiltersFromConfig<T>(config);
return filterCollection(collection, filters, astroConfig);
}

View File

@ -1,10 +1,10 @@
import { get_cached_object, set_cached_object, rm_cached_object } from "@polymech/cache"
import { run, OptionsSchema } from "@polymech/kbot-d";
import { resolveVariables } from "@polymech/commons/variables"
import { } from "@polymech/core/objects"
import { logger, env } from "./index.js"
import { removeEmptyObjects } from "@/base/objects.js"
import { LLM_CACHE } from "@/config/config.js"
import { removeEmptyObjects } from "./objects.js"
const LLM_CACHE = true
import {
TemplateProps,

View File

@ -46,7 +46,7 @@ export interface Props {
const {
images = [],
glob: globPattern,
maxItems = 50,
maxItems = 150,
maxWidth = "300px",
maxHeight = "400px",
entryPath,
@ -406,42 +406,37 @@ if (groupingFunction) {
@touchcancel="isSwiping = false;"
>
{finalImages.map((image, index) => (
<div
x-show={`currentIndex === ${index}`}
class="lightbox-container"
style="display: grid; grid-template: 'container' 1fr; place-items: center; place-content: center;"
>
<Img
src={image.src}
alt={image.alt}
placeholder="blurred"
format="avif"
objectFit="contain"
sizes={mergedLightboxSettings.SIZES_LARGE}
attributes={{
img: {
class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg",
style: "display: block; width: auto; height: auto; grid-area: container;"
}
}}
/>
{(mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) && (
<div
class="max-h-[32vh] text-white bg-black/50 rounded-b-lg p-2 overflow-y-auto"
style="grid-area: container; place-self: end center; width: 100%;"
>
{mergedLightboxSettings.SHOW_TITLE && image.title && (
<h3 class="text-xl mb-2">
<Translate>{image.title}</Translate>
</h3>
)}
{mergedLightboxSettings.SHOW_DESCRIPTION && image.description && (
<p>
<Translate>{image.description}</Translate>
</p>
)}
</div>
)}
<div x-show={`currentIndex === ${index}`} class="flex items-center justify-center">
<div class="relative">
<Img
src={image.src}
alt={image.alt}
placeholder="blurred"
format="avif"
objectFit="contain"
sizes={mergedLightboxSettings.SIZES_LARGE}
attributes={{
img: {
class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg",
style: "display: block !important; width: auto; height: auto;"
}
}}
/>
{(mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) && (
<div class="absolute bottom-0 left-0 right-0 m-[8px] max-h-[32vh] text-white bg-black/50 rounded-lg p2">
{mergedLightboxSettings.SHOW_TITLE && image.title && (
<h3 class="text-xl mb-2">
<Translate>{image.title}</Translate>
</h3>
)}
{mergedLightboxSettings.SHOW_DESCRIPTION && image.description && (
<p>
<Translate>{image.description}</Translate>
</p>
)}
</div>
)}
</div>
</div>
))}

View File

@ -1,4 +1,5 @@
import { PAGE_TITLE_ID } from '../../constants';
/** Identifier for the page title h1 when it is injected into the ToC. */
export const PAGE_TITLE_ID = 'starlight__overview';
export class StarlightTOC extends HTMLElement {
private _current = this.querySelector<HTMLAnchorElement>('a[aria-current="true"]');
@ -22,7 +23,7 @@ export class StarlightTOC extends HTMLElement {
private init = (): void => {
/** All the links in the table of contents. */
const links = [...this.querySelectorAll('a')];
const links = Array.from(this.querySelectorAll('a'));
/** Test if an element is a table-of-contents heading. */
const isHeading = (el: Element): el is HTMLHeadingElement => {

View File

@ -24,7 +24,7 @@ export function generateToC(
/** Inject a ToC entry as deep in the tree as its `depth` property requires. */
function injectChild(items: TocItem[], item: TocItem): void {
const lastItem = items.at(-1);
const lastItem = items[items.length - 1];
if (!lastItem || lastItem.depth >= item.depth) {
items.push(item);
} else {

View File

@ -45,8 +45,7 @@ function getElementHash(element: Element): string {
}
export function storeSidebarState() {
console.log('[Sidebar] Storing state...');
const items = getSidebarItems();
const items = Array.from(getSidebarItems());
for (const item of items) {
const details = item.querySelector('details');
if (details) {
@ -54,37 +53,22 @@ export function storeSidebarState() {
const key = `sidebar-group-${hash}`;
const value = String(details.open);
sessionStorage.setItem(key, value);
console.log(`[Sidebar] Stored: ${key} = ${value}`);
}
}
}
export function restoreSidebarState() {
console.log('[Sidebar] Restoring state...');
const items = getSidebarItems();
console.log(`[Sidebar] Found ${items.length} sidebar groups`);
const items = Array.from(getSidebarItems());
for (const item of items) {
const hash = getElementHash(item);
const key = `sidebar-group-${hash}`;
const storedState = sessionStorage.getItem(key);
const details = item.querySelector('details');
console.log(`[Sidebar] Processing group with hash: ${hash}`);
console.log(`[Sidebar] Stored state for ${key}: ${storedState}`);
console.log(`[Sidebar] Details element found: ${!!details}`);
if (details) {
console.log(`[Sidebar] Current details.open before restore: ${details.open}`);
const details = item.querySelector('details');
if (details) {
if (storedState !== null) {
details.open = storedState === 'true';
console.log(`[Sidebar] Restored: ${key} = ${details.open}`);
} else {
console.log(`[Sidebar] No stored state found for ${key}, keeping current state: ${details.open}`);
}
console.log(`[Sidebar] Final details.open after restore: ${details.open}`);
}
}
}
}

View File

@ -1,6 +1,7 @@
import { getCollection } from 'astro:content';
import type { SidebarGroup, SidebarLink, SortFunction } from './types.js';
import path from 'path';
import { filterCollection, defaultFilters, type CollectionFilter } from '../../base/collections.js';
import { z } from 'zod';
interface DirectoryStructure {
[key: string]: {
@ -9,8 +10,50 @@ interface DirectoryStructure {
};
}
// Zod schema for sidebar generation options
export const SidebarGenerationOptionsSchema = z.object({
currentPath: z.string().optional(),
maxDepth: z.number().min(0).max(10).default(2),
collapsedByDefault: z.boolean().default(false),
currentDepth: z.number().min(0).default(0),
sortBy: z.enum(['alphabetical', 'date', 'custom']).default('alphabetical'),
customSort: z.any().optional(), // We'll validate this at runtime
filters: z.array(z.any()).optional(), // We'll validate this at runtime
}).strict();
// Define the proper TypeScript types
export interface SidebarGenerationOptions {
currentPath?: string;
maxDepth: number;
collapsedByDefault: boolean;
currentDepth: number;
sortBy: 'alphabetical' | 'date' | 'custom';
customSort?: (a: SidebarLink | SidebarGroup, b: SidebarLink | SidebarGroup) => number;
filters?: CollectionFilter[];
}
/**
* Create validated sidebar generation options
* @param options - Partial options to validate and fill with defaults
* @returns Validated options with defaults applied
*/
export function createSidebarOptions(options: Partial<SidebarGenerationOptions> = {}): SidebarGenerationOptions {
const parsed = SidebarGenerationOptionsSchema.parse(options);
// Runtime validation for functions
if (parsed.customSort && typeof parsed.customSort !== 'function') {
throw new Error('customSort must be a function');
}
if (parsed.filters && (!Array.isArray(parsed.filters) || parsed.filters.some(f => typeof f !== 'function'))) {
throw new Error('filters must be an array of functions');
}
return parsed as SidebarGenerationOptions;
}
/**
* Generate nested sidebar structure from a content collection directory
* @deprecated Use generateLinksFromDirectoryWithConfig with options object instead
*/
export async function generateLinksFromDirectory(
directory: string,
@ -18,23 +61,51 @@ export async function generateLinksFromDirectory(
maxDepth: number = 2,
currentDepth: number = 0
): Promise<(SidebarLink | SidebarGroup)[]> {
return generateLinksFromDirectoryWithConfig(directory, currentPath, maxDepth, false, currentDepth);
return generateLinksFromDirectoryWithConfig(directory, {
currentPath,
maxDepth,
currentDepth,
collapsedByDefault: false
});
}
/**
* Generate nested sidebar structure with configuration support
* @param directory - The collection directory to generate links from
* @param options - Configuration options for sidebar generation
*
* @example
* ```typescript
* // Basic usage with defaults
* const links = await generateLinksFromDirectoryWithConfig('resources');
*
* // With custom options
* const links = await generateLinksFromDirectoryWithConfig('resources', {
* maxDepth: 3,
* collapsedByDefault: true,
* sortBy: 'date',
* filters: [hasTitle, isNotDraft]
* });
*
* // Using the helper function
* const options = createSidebarOptions({
* maxDepth: 4,
* sortBy: 'custom',
* customSort: (a, b) => a.label.localeCompare(b.label)
* });
* const links = await generateLinksFromDirectoryWithConfig('resources', options);
* ```
*/
export async function generateLinksFromDirectoryWithConfig(
directory: string,
currentPath?: string,
maxDepth: number = 2,
collapsedByDefault: boolean = false,
currentDepth: number = 0,
sortBy: SortFunction = 'alphabetical',
customSort?: (a: SidebarLink | SidebarGroup, b: SidebarLink | SidebarGroup) => number
directory: string,
options: Partial<SidebarGenerationOptions> = {}
): Promise<(SidebarLink | SidebarGroup)[]> {
try {
const entries = await getCollection(directory as any);
// Validate and apply defaults to options
const validatedOptions = SidebarGenerationOptionsSchema.parse(options);
const allEntries = await getCollection(directory as any);
const entries = filterCollection(allEntries, validatedOptions.filters || defaultFilters);
// Organize entries by directory structure
const structure = organizeByDirectory(entries);
@ -42,12 +113,12 @@ export async function generateLinksFromDirectoryWithConfig(
return buildSidebarFromStructure(
structure,
directory,
currentPath,
maxDepth,
currentDepth,
collapsedByDefault,
sortBy,
customSort,
validatedOptions.currentPath,
validatedOptions.maxDepth,
validatedOptions.currentDepth,
validatedOptions.collapsedByDefault,
validatedOptions.sortBy,
validatedOptions.customSort,
entries
);
} catch (error) {
@ -108,7 +179,6 @@ function buildSidebarFromStructure(
entries?: any[]
): (SidebarLink | SidebarGroup)[] {
const items: (SidebarLink | SidebarGroup)[] = [];
// Process root level files first
if (structure['']?.files) {
const rootFiles = structure[''].files
@ -292,12 +362,14 @@ export async function processSidebarGroup(group: SidebarGroup, currentPath?: str
const sortBy = group.autogenerate.sortBy ?? 'alphabetical';
const items = await generateLinksFromDirectoryWithConfig(
group.autogenerate.directory,
currentPath,
maxDepth,
group.autogenerate.collapsed ?? false, // Pass collapsed config to subdirectories
0, // currentDepth starts at 0
sortBy,
group.autogenerate.customSort
{
currentPath,
maxDepth,
collapsedByDefault: group.autogenerate.collapsed ?? false,
currentDepth: 0,
sortBy,
customSort: group.autogenerate.customSort
}
);
processedGroup.items = items;
processedGroup.collapsed = group.autogenerate.collapsed ?? group.collapsed;

View File

@ -1,5 +1,5 @@
// Access to extended Astro config
import type { SidebarGroup } from '@/components/sidebar/types';
import type { SidebarGroup } from '../components/sidebar/types.js';
// Define extended config interface
interface ExtendedAstroConfig {

View File

@ -4,6 +4,9 @@ export const foo2 = 2
// export { default as i18n } from './components/i18n.astro'
// export { default as Test } from './components/test.astro'
// Export collection utilities
export * from './base/collections'