refactor pm base | config
This commit is contained in:
parent
940be340fa
commit
1793db09a0
@ -1,12 +1,12 @@
|
||||
export default {
|
||||
"environment": "dev",
|
||||
"environment": "build",
|
||||
"isSsrBuild": false,
|
||||
"projectBase": "",
|
||||
"publicDir": "C:\\Users\\zx\\Desktop\\polymech\\library.polymech\\public\\",
|
||||
"rootDir": "C:\\Users\\zx\\Desktop\\polymech\\library.polymech\\",
|
||||
"mode": "dev",
|
||||
"outDir": "dist",
|
||||
"assetsDir": "/_astro",
|
||||
"mode": "production",
|
||||
"outDir": "C:\\Users\\zx\\Desktop\\polymech\\library.polymech\\dist\\",
|
||||
"assetsDir": "_astro",
|
||||
"sourcemap": false,
|
||||
"assetFileNames": "/_astro/[name]@[width].[hash][extname]"
|
||||
}
|
||||
@ -46,10 +46,8 @@ const configFile = findUpSync([
|
||||
"astro-imagetools.config.js",
|
||||
"astro-imagetools.config.mjs",
|
||||
]);
|
||||
|
||||
const configFunction = configFile
|
||||
? await import(configFile).catch(async () => await import("/" + configFile))
|
||||
: null;
|
||||
/* @vite-ignore */
|
||||
const configFunction = configFile ? await import(configFile).catch(async () => await import("/" + configFile)) : null;
|
||||
|
||||
const rawGlobalConfigOptions = configFunction?.default ?? {};
|
||||
|
||||
@ -93,7 +91,7 @@ const cache_dir = () => {
|
||||
if (!exists(dir)) {
|
||||
mkdir(dir);
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
dir = GlobalConfigOptions.cacheRoot
|
||||
}
|
||||
return dir + "/";
|
||||
|
||||
@ -7,6 +7,7 @@ import { hideBin } from 'yargs/helpers';
|
||||
import { substitute } from "@polymech/commons/variables";
|
||||
import { appConfigSchema } from "./config.schema.js";
|
||||
import type { AppConfig } from "./config.schema.js";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
const I18N_SOURCE_LANGUAGE = 'en';
|
||||
|
||||
@ -40,14 +41,15 @@ function deepMerge(target: any, source: any): any {
|
||||
|
||||
export function loadConfig(
|
||||
locale: string = I18N_SOURCE_LANGUAGE,
|
||||
libraryPath: string = LIBRARY_CONFIG_PATH
|
||||
config: string = LIBRARY_CONFIG_PATH,
|
||||
schema: z.ZodType<any> = appConfigSchema,
|
||||
): AppConfig {
|
||||
// 1. Load Library Config (Defaults)
|
||||
let rawLibraryContent: string;
|
||||
try {
|
||||
rawLibraryContent = fs.readFileSync(libraryPath, 'utf-8');
|
||||
rawLibraryContent = fs.readFileSync(config, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read library config file at ${libraryPath}: ${error}`);
|
||||
throw new Error(`Failed to read library config file at ${config}: ${error}`);
|
||||
}
|
||||
|
||||
const variables = {
|
||||
@ -61,7 +63,6 @@ export function loadConfig(
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse library config JSON: ${error}`);
|
||||
}
|
||||
|
||||
// 2. Parse CLI Arguments
|
||||
// We assume the caller might want to pass args, or we just grab process.argv
|
||||
// We cast to any because yargs returns a complex type
|
||||
@ -69,7 +70,7 @@ export function loadConfig(
|
||||
|
||||
// 3. Determine User Config Path
|
||||
// Check for --config <path>
|
||||
const userConfigPath = argv.config ? path.resolve(argv.config) : USER_CONFIG_DEFAULT_PATH;
|
||||
const userConfigPath = argv.config ? path.resolve(argv.config) : LIBRARY_CONFIG_PATH;
|
||||
|
||||
// 4. Load User Config (if exists)
|
||||
let userConfig: any = {};
|
||||
@ -81,8 +82,11 @@ export function loadConfig(
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load or parse user config at ${userConfigPath}: ${error}`);
|
||||
}
|
||||
} else {
|
||||
console.log('User config not found at', userConfigPath);
|
||||
}
|
||||
|
||||
|
||||
// 5. Merge: Library <- User <- CLI
|
||||
// Note: yargs parses --config as part of argv, but also other flags like --core.logging_namespace
|
||||
// We filter out specific known CLI-only flags if needed, but config schema validation will drop unknown keys anyway?
|
||||
@ -92,17 +96,11 @@ export function loadConfig(
|
||||
// CLI args often come with standard keys like '$0', '_' which we might want to exclude if we blindly merge.
|
||||
// However, deepMerge will add them.
|
||||
// Ideally we would only merge keys that exist in the schema, but dynamic is fine for now.
|
||||
|
||||
let mergedConfig = deepMerge(libraryConfig, userConfig);
|
||||
mergedConfig = deepMerge(mergedConfig, argv);
|
||||
|
||||
// 6. Validate
|
||||
const result = appConfigSchema.safeParse(mergedConfig);
|
||||
// @todo 6. Validate
|
||||
// const result = schema.parse(mergedConfig);
|
||||
|
||||
if (!result.success) {
|
||||
// Pretty print error if possible or just message
|
||||
throw new Error(`Config validation failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
@ -62,6 +62,7 @@ export const devSchema = z.object({
|
||||
export const i18NSchema = z.object({
|
||||
store: z.string(),
|
||||
cache: z.boolean(),
|
||||
source_language: z.string(),
|
||||
asset_path: z.string()
|
||||
});
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { sanitizeUri } from 'micromark-util-sanitize-uri'
|
||||
import { loadConfig } from './config-loader.js'
|
||||
|
||||
export const I18N_SOURCE_LANGUAGE = 'en'
|
||||
|
||||
// Load config
|
||||
const config = loadConfig(I18N_SOURCE_LANGUAGE)
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { translate } from '../base/i18n.js'
|
||||
import { I18N_SOURCE_LANGUAGE } from 'config/config.js'
|
||||
import { I18N_SOURCE_LANGUAGE } from '../app/config.js'
|
||||
import { loadConfig } from './config-loader.js'
|
||||
import pMap from 'p-map'
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import type { CollectionEntry } from 'astro:content';
|
||||
import { isFolder } from '@polymech/commons';
|
||||
import { parseFrontmatter } from '@astrojs/markdown-remark';
|
||||
import { translate } from './i18n.js';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "../app/config.js";
|
||||
|
||||
// Filter function type
|
||||
export type CollectionFilter<T = any> = (entry: CollectionEntry<T>, astroConfig?: any) => boolean;
|
||||
|
||||
@ -3,15 +3,22 @@ import { resolve } from '@polymech/commons'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
|
||||
import type { IOptions } from '@polymech/i18n'
|
||||
import { CONFIG_DEFAULT } from '@polymech/commons'
|
||||
import { I18N_ASSET_PATH, I18N_CACHE, I18N_SOURCE_LANGUAGE, PRODUCT_SPECS, RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS } from '@/app/config.js'
|
||||
export type { IOptions } from '@polymech/i18n'
|
||||
|
||||
import { CONFIG_DEFAULT } from '@polymech/commons'
|
||||
import { translateXLS } from '@polymech/i18n/translate_xls'
|
||||
import { I18N_STORE, OSR_ROOT } from 'config/config.js'
|
||||
import { translateText } from '@polymech/i18n/translate_text'
|
||||
|
||||
|
||||
import { PolymechInstance } from '../registry.js';
|
||||
import { AppConfig } from "../app/config.schema.js"
|
||||
|
||||
import { I18N_STORE, OSR_ROOT } from '../app/config.js'
|
||||
import { I18N_ASSET_PATH, I18N_CACHE, PRODUCT_SPECS, RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS } from '../app/config.js'
|
||||
|
||||
import { logger } from './index.js'
|
||||
|
||||
export type { IOptions } from '@polymech/i18n'
|
||||
const loadConfig = (): AppConfig => PolymechInstance.getConfig();
|
||||
|
||||
export const translate = async (text: string, srcLanguage = 'en', targetLanguage, opts = {}) => {
|
||||
if (!targetLanguage) {
|
||||
@ -20,7 +27,7 @@ export const translate = async (text: string, srcLanguage = 'en', targetLanguage
|
||||
try {
|
||||
const store = I18N_STORE(OSR_ROOT(), targetLanguage)
|
||||
let translation = text
|
||||
translation = await translateText(text, srcLanguage, targetLanguage, {
|
||||
translation = await translateText(text, srcLanguage, targetLanguage, {
|
||||
store,
|
||||
...opts
|
||||
})
|
||||
@ -31,19 +38,20 @@ export const translate = async (text: string, srcLanguage = 'en', targetLanguage
|
||||
return text
|
||||
}
|
||||
export const translateSheets = async (product, language) => {
|
||||
const config: any = CONFIG_DEFAULT()
|
||||
if (language === I18N_SOURCE_LANGUAGE) {
|
||||
const pm_config: any = CONFIG_DEFAULT()
|
||||
const config = loadConfig()
|
||||
if (language === config.i18n.source_language) {
|
||||
return
|
||||
}
|
||||
const i18nOptions: IOptions = {
|
||||
srcLang: I18N_SOURCE_LANGUAGE,
|
||||
srcLang: config.i18n.source_language,
|
||||
dstLang: language,
|
||||
src: PRODUCT_SPECS(product),
|
||||
store: I18N_STORE(OSR_ROOT(), language),
|
||||
dst: I18N_ASSET_PATH,
|
||||
query: "$[*][0,1,2,3]",
|
||||
cache: I18N_CACHE,
|
||||
api_key: config.deepl.auth_key,
|
||||
api_key: pm_config.deepl.auth_key,
|
||||
logLevel: RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS
|
||||
}
|
||||
const src = `${PRODUCT_SPECS(product)}`
|
||||
|
||||
@ -9,17 +9,11 @@ import { findUp } from 'find-up'
|
||||
import { createLogger } from '@polymech/log'
|
||||
import { parse, IProfile } from '@polymech/commons/profile'
|
||||
|
||||
import { translate } from "@/base/i18n.js"
|
||||
// import { renderMarkup } from "../model/component.js"
|
||||
|
||||
import {
|
||||
LOGGING_NAMESPACE,
|
||||
OSRL_ENV,
|
||||
OSRL_PRODUCT_PROFILE,
|
||||
PRODUCT_ROOT,
|
||||
I18N_SOURCE_LANGUAGE
|
||||
} from 'config/config.js'
|
||||
|
||||
PRODUCT_ROOT
|
||||
} from '../app/config.js'
|
||||
|
||||
export const logger = createLogger('polymech-astro')
|
||||
|
||||
|
||||
@ -14,9 +14,9 @@ import { files } from '@polymech/commons'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
|
||||
import { logger } from '@/base/index.js'
|
||||
import { logger } from './index.js'
|
||||
|
||||
import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.js'
|
||||
import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from './objects.js'
|
||||
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL, FILE_SERVER_DEV } from '../app/config.js'
|
||||
import { GalleryImage, MetaJSON } from './images.js'
|
||||
|
||||
|
||||
@ -2,5 +2,6 @@
|
||||
import { cli } from './cli.js';
|
||||
import './commands/build-config.js';
|
||||
import './commands/build.js';
|
||||
import './commands/dev.js';
|
||||
|
||||
cli.parse();
|
||||
|
||||
@ -18,7 +18,7 @@ export const builder = (yargs: CLI.Argv) => {
|
||||
alias: 'd',
|
||||
type: 'string',
|
||||
describe: 'Output d.ts path',
|
||||
default: './src/app/config.d.ts'
|
||||
default: './src/app/config-types.ts'
|
||||
})
|
||||
.option('dest-schema', {
|
||||
alias: 'z',
|
||||
|
||||
35
packages/polymech/src/commands/dev.ts
Normal file
35
packages/polymech/src/commands/dev.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as CLI from 'yargs';
|
||||
import { cli } from '../cli.js';
|
||||
import { handler as configHandler, builder as configBuilder } from './build-config.js';
|
||||
|
||||
export const command = 'dev [src]';
|
||||
export const desc = 'Generate config and start Astro dev server';
|
||||
|
||||
export const builder = (yargs: CLI.Argv) => {
|
||||
return configBuilder(yargs)
|
||||
.strict(false);
|
||||
};
|
||||
|
||||
export async function handler(argv: CLI.Arguments) {
|
||||
// 1. Config Generation
|
||||
await configHandler(argv);
|
||||
|
||||
// 2. Astro Dev
|
||||
const devIndex = process.argv.indexOf('dev');
|
||||
const forwardedArgs = devIndex !== -1 ? process.argv.slice(devIndex + 1) : [];
|
||||
|
||||
console.log('[pm-astro] Running astro dev...');
|
||||
// Execute astro dev
|
||||
// Standard dev args like --host can be passed by user
|
||||
const result = spawnSync('npx', ['astro', 'dev', ...forwardedArgs], {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
}
|
||||
|
||||
cli.command(command, desc, builder, handler);
|
||||
@ -4,18 +4,17 @@ import "../styles/flowbite.css"
|
||||
import "../styles/global.css"
|
||||
import "../styles/custom.scss"
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { AstroSeo } from "@astrolib/seo"
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE } from "../app/config.js"
|
||||
import { translate } from '@polymech/astro-base/base/i18n.js'
|
||||
import { item_defaults } from '@/base/index.js'
|
||||
|
||||
import { LANGUAGES_PROD } from "config/config.js"
|
||||
import config from "config/config.json"
|
||||
import { LANGUAGES_PROD } from "../app/config.js"
|
||||
import config from "../app/config.json"
|
||||
import { plainify } from "@polymech/astro-base/base/strings.js"
|
||||
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
|
||||
import { AstroSeo } from "@astrolib/seo"
|
||||
|
||||
import StructuredData from '@polymech/astro-base/components/ArticleStructuredData.astro'
|
||||
import Hreflang from '@polymech/astro-base/components/hreflang.astro'
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
---
|
||||
import { Img } from "imagetools/components";
|
||||
import Translate from "./i18n.astro"
|
||||
import { translate } from "@/base/i18n";
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE, IMAGE_SETTINGS } from "config/config.js"
|
||||
import Translate from "./i18n.astro"
|
||||
import { translate } from "../base/i18n";
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE, IMAGE_SETTINGS } from "../app/config.js"
|
||||
|
||||
interface Image {
|
||||
alt: string
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
---
|
||||
import { Img } from "imagetools/components";
|
||||
|
||||
import { globBase, pathInfo } from "@polymech/commons";
|
||||
import Translate from "./i18n.astro";
|
||||
import { IMAGE_SETTINGS } from "config/config.js";
|
||||
import { IMAGE_SETTINGS } from "../app/config.js";
|
||||
import path from "node:path";
|
||||
import { glob } from 'glob';
|
||||
import { globBase, pathInfo } from "@polymech/commons";
|
||||
import { extractImageMetadata, groupByYear, groupByMonth, GroupInfo } from "../base/media";
|
||||
import { resolveImagePath } from "../utils/path-resolution.js";
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import Wrapper from "@/components/containers/Wrapper.astro";
|
||||
import { footer_left, footer_right } from "../../app/navigation.js";
|
||||
import { ISO_LANGUAGE_LABELS } from "@polymech/i18n";
|
||||
import { LANGUAGES_PROD, I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { LANGUAGES_PROD, I18N_SOURCE_LANGUAGE } from "../../app/config.js";
|
||||
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE;
|
||||
const currentUrl = new URL(Astro.url);
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
---
|
||||
import Wrapper from "@/components/containers/Wrapper.astro";
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE } from "@/app/config";
|
||||
|
||||
import { items } from "../../app/navigation.js";
|
||||
|
||||
import ThemeToggle from "./ThemeToggle.astro";
|
||||
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE;
|
||||
import { PolymechInstance } from "../../registry.js";
|
||||
import { AppConfig } from "@/app/config.schema.js";
|
||||
const config = (): AppConfig => PolymechInstance.getConfig();
|
||||
const locale = Astro.currentLocale || config().i18n.source_language;
|
||||
const navItems = await items({ locale });
|
||||
---
|
||||
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
---
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
|
||||
import { translate, IOptions } from '@/base/i18n.js'
|
||||
import { I18N_SOURCE_LANGUAGE } from "../app/config.js";
|
||||
import { translate, IOptions } from "../base/i18n.js";
|
||||
|
||||
export interface Props extends IOptions {
|
||||
language?: string,
|
||||
clazz?:string
|
||||
export interface Props extends IOptions {
|
||||
language?: string;
|
||||
clazz?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
language = Astro.currentLocale,
|
||||
clazz = '',
|
||||
...rest
|
||||
} = Astro.props
|
||||
const { language = Astro.currentLocale, clazz = "", ...rest } = Astro.props;
|
||||
|
||||
const content = await Astro.slots.render('default')
|
||||
const translatedText = await translate(content, I18N_SOURCE_LANGUAGE, language, rest)
|
||||
const content = await Astro.slots.render("default");
|
||||
const translatedText = await translate(
|
||||
content,
|
||||
I18N_SOURCE_LANGUAGE,
|
||||
language,
|
||||
rest,
|
||||
);
|
||||
---
|
||||
|
||||
<p data-widget="polymech.i18n" class={clazz}>
|
||||
{translatedText}
|
||||
{translatedText}
|
||||
</p>
|
||||
|
||||
@ -1,43 +1,44 @@
|
||||
---
|
||||
import { getProcessedCoilsData } from '../../utils/modbus-data';
|
||||
import { getModbusFunctionName, getFunctionCategory } from '../../utils/modbusUtils';
|
||||
import { getProcessedCoilsData } from "../../utils/modbus-data";
|
||||
|
||||
const groupedCoils = getProcessedCoilsData();
|
||||
const groupNames = Object.keys(groupedCoils).sort();
|
||||
---
|
||||
|
||||
<div class="modbus-coils-tables">
|
||||
{groupNames.map(groupName => (
|
||||
<div class="group-section">
|
||||
<h3>{groupName}</h3>
|
||||
<div class="table-container">
|
||||
<table class="modbus-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FC</th>
|
||||
<th>Address</th>
|
||||
<th>Name</th>
|
||||
<th>Component</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedCoils[groupName].map(coil => {
|
||||
return (
|
||||
<tr>
|
||||
<td class="fc-cell">1/5</td>
|
||||
<td>{coil.address}</td>
|
||||
<td>{coil.name}</td>
|
||||
<td>{coil.component}</td>
|
||||
<td>{coil.id}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{
|
||||
groupNames.map((groupName) => (
|
||||
<div class="group-section">
|
||||
<h3>{groupName}</h3>
|
||||
<div class="table-container">
|
||||
<table class="modbus-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FC</th>
|
||||
<th>Address</th>
|
||||
<th>Name</th>
|
||||
<th>Component</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedCoils[groupName].map((coil) => {
|
||||
return (
|
||||
<tr>
|
||||
<td class="fc-cell">1/5</td>
|
||||
<td>{coil.address}</td>
|
||||
<td>{coil.name}</td>
|
||||
<td>{coil.component}</td>
|
||||
<td>{coil.id}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@ -108,7 +109,7 @@ const groupNames = Object.keys(groupedCoils).sort();
|
||||
.modbus-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.modbus-table th,
|
||||
.modbus-table td {
|
||||
padding: 0.5rem;
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
import {
|
||||
getProcessedRegistersData,
|
||||
getRegisterDescription,
|
||||
} from "@polymech/astro-base/utils/modbus-data.js";
|
||||
} from "../../utils/modbus-data";
|
||||
|
||||
import { parseRegisterName } from "@polymech/astro-base/utils/modbusUtils.js";
|
||||
import { parseRegisterName } from "../../utils/modbusUtils";
|
||||
|
||||
const groupedRegisters = getProcessedRegistersData();
|
||||
const groupNames = Object.keys(groupedRegisters).sort();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import { createMarkdownComponent } from "@/base/index.js";
|
||||
import { translate } from "@/base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE, ASSET_URL } from "config/config.js";
|
||||
import { createMarkdownComponent } from "../../base/index.js";
|
||||
import { translate } from "../../base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE, ASSET_URL } from "../../app/config.js";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import { toMarkdown } from "mdast-util-to-markdown";
|
||||
import { visit } from "unist-util-visit";
|
||||
@ -14,6 +14,7 @@ interface Props {
|
||||
translate?: boolean;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
const {
|
||||
markdown,
|
||||
className = "",
|
||||
|
||||
@ -1,38 +1,9 @@
|
||||
---
|
||||
/**
|
||||
* ResourceCard Component
|
||||
*
|
||||
* A flexible card component for displaying content from any collection.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* // For resources collection (default)
|
||||
* <ResourceCard
|
||||
* title="Article Title"
|
||||
* url="/en/resources/article"
|
||||
* // ... other props
|
||||
* />
|
||||
*
|
||||
* // For store collection
|
||||
* <ResourceCard
|
||||
* title="Product Name"
|
||||
* url="/en/store/product"
|
||||
* collectionName="store"
|
||||
* // ... other props
|
||||
* />
|
||||
*
|
||||
* // For helpcenter collection
|
||||
* <ResourceCard
|
||||
* title="Help Article"
|
||||
* url="/en/helpcenter/article"
|
||||
* collectionName="helpcenter"
|
||||
* // ... other props
|
||||
* />
|
||||
*/
|
||||
|
||||
import { Img } from "imagetools/components";
|
||||
import { translate } from "@polymech/astro-base/base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { resolveImagePath } from '@/utils/path-resolution';
|
||||
import { translate } from "../../base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "../../app/config.js";
|
||||
import { resolveImagePath } from "../../utils/path-resolution.js";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
---
|
||||
import SidebarGroup from './SidebarGroup.astro';
|
||||
import TableOfContentsWithScroll from './TableOfContentsWithScroll.astro';
|
||||
import { processSidebarGroup, getCurrentPath } from './utils';
|
||||
import type { SidebarGroup as SidebarGroupType } from './types';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import SidebarPersister from './SidebarPersister.astro';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import SidebarGroup from "./SidebarGroup.astro";
|
||||
import TableOfContentsWithScroll from "./TableOfContentsWithScroll.astro";
|
||||
import { processSidebarGroup, getCurrentPath } from "./utils";
|
||||
import type { SidebarGroup as SidebarGroupType } from "./types";
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import SidebarPersister from "./SidebarPersister.astro";
|
||||
import { I18N_SOURCE_LANGUAGE } from "../../app/config.js";
|
||||
|
||||
interface Props {
|
||||
config: SidebarGroupType[];
|
||||
@ -15,12 +15,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const { config, currentUrl, headings = [], pageNavigation = [] } = Astro.props;
|
||||
const currentPath = currentUrl ? getCurrentPath(currentUrl) : '';
|
||||
const currentPath = currentUrl ? getCurrentPath(currentUrl) : "";
|
||||
|
||||
// Extract locale from URL path (e.g., /es/resources/ -> 'es')
|
||||
let locale: string = I18N_SOURCE_LANGUAGE; // Default to source language
|
||||
if (currentPath) {
|
||||
const pathSegments = currentPath.split('/').filter(segment => segment);
|
||||
const pathSegments = currentPath.split("/").filter((segment) => segment);
|
||||
// Check if first segment is a language code (2 letters)
|
||||
if (pathSegments.length > 0 && /^[a-z]{2}$/.test(pathSegments[0])) {
|
||||
locale = pathSegments[0];
|
||||
@ -29,32 +29,32 @@ if (currentPath) {
|
||||
|
||||
// Process all sidebar groups
|
||||
const processedGroups = await Promise.all(
|
||||
config.map(group => processSidebarGroup(group, currentPath, locale))
|
||||
config.map((group) => processSidebarGroup(group, currentPath, locale)),
|
||||
);
|
||||
|
||||
// Process page-level navigation
|
||||
const processedPageNav = await Promise.all(
|
||||
pageNavigation.map(group => processSidebarGroup({...group, isPageLevel: true}, currentPath, locale))
|
||||
pageNavigation.map((group) =>
|
||||
processSidebarGroup({ ...group, isPageLevel: true }, currentPath, locale),
|
||||
),
|
||||
);
|
||||
---
|
||||
|
||||
<nav class="sidebar-nav" aria-label="Site navigation">
|
||||
<div class="sidebar-content">
|
||||
{/* Page-level navigation first */}
|
||||
{processedPageNav.map((group) => (
|
||||
<SidebarGroup group={group} />
|
||||
))}
|
||||
|
||||
{processedPageNav.map((group) => <SidebarGroup group={group} />)}
|
||||
|
||||
{/* Global navigation */}
|
||||
{processedGroups.map((group) => (
|
||||
<SidebarGroup group={group} />
|
||||
))}
|
||||
|
||||
{processedGroups.map((group) => <SidebarGroup group={group} />)}
|
||||
|
||||
{/* Table of contents */}
|
||||
{headings && headings.length > 0 && (
|
||||
<TableOfContentsWithScroll headings={headings} />
|
||||
)}
|
||||
{
|
||||
headings && headings.length > 0 && (
|
||||
<TableOfContentsWithScroll headings={headings} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<SidebarPersister />
|
||||
<SidebarPersister />
|
||||
|
||||
@ -2,7 +2,7 @@ import { getCollection } from 'astro:content';
|
||||
import type { SidebarGroup, SidebarLink, SortFunction } from './types.js';
|
||||
import { filterCollection, defaultFilters, type CollectionFilter } from '../../base/collections.js';
|
||||
import { z } from 'zod';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "../../app/config.js";
|
||||
|
||||
interface DirectoryStructure {
|
||||
[key: string]: {
|
||||
@ -42,7 +42,7 @@ export interface SidebarGenerationOptions {
|
||||
*/
|
||||
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');
|
||||
@ -50,7 +50,7 @@ export function createSidebarOptions(options: Partial<SidebarGenerationOptions>
|
||||
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;
|
||||
}
|
||||
|
||||
@ -59,8 +59,8 @@ export function createSidebarOptions(options: Partial<SidebarGenerationOptions>
|
||||
* @deprecated Use generateLinksFromDirectoryWithConfig with options object instead
|
||||
*/
|
||||
export async function generateLinksFromDirectory(
|
||||
directory: string,
|
||||
currentPath?: string,
|
||||
directory: string,
|
||||
currentPath?: string,
|
||||
maxDepth: number = 2,
|
||||
currentDepth: number = 0
|
||||
): Promise<(SidebarLink | SidebarGroup)[]> {
|
||||
@ -107,19 +107,19 @@ export async function generateLinksFromDirectoryWithConfig(
|
||||
try {
|
||||
// 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);
|
||||
|
||||
|
||||
return buildSidebarFromStructure(
|
||||
structure,
|
||||
directory,
|
||||
validatedOptions.currentPath,
|
||||
validatedOptions.maxDepth,
|
||||
validatedOptions.currentDepth,
|
||||
structure,
|
||||
directory,
|
||||
validatedOptions.currentPath,
|
||||
validatedOptions.maxDepth,
|
||||
validatedOptions.currentDepth,
|
||||
validatedOptions.collapsedByDefault,
|
||||
validatedOptions.sortBy,
|
||||
validatedOptions.customSort,
|
||||
@ -136,36 +136,36 @@ export async function generateLinksFromDirectoryWithConfig(
|
||||
* Organize entries into a directory structure
|
||||
*/
|
||||
function organizeByDirectory(entries: any[]): DirectoryStructure {
|
||||
const structure: DirectoryStructure = {
|
||||
'': { files: [], subdirs: {} }
|
||||
};
|
||||
const structure: DirectoryStructure = {
|
||||
'': { files: [], subdirs: {} }
|
||||
};
|
||||
|
||||
entries.forEach(entry => {
|
||||
const entryPath = entry.id;
|
||||
const parts = entryPath.split('/');
|
||||
parts.pop();
|
||||
|
||||
let parentSubdirs = structure[''].subdirs;
|
||||
parts.forEach(part => {
|
||||
if (!parentSubdirs[part]) {
|
||||
parentSubdirs[part] = { files: [], subdirs: {} };
|
||||
}
|
||||
parentSubdirs = parentSubdirs[part].subdirs;
|
||||
});
|
||||
entries.forEach(entry => {
|
||||
const entryPath = entry.id;
|
||||
const parts = entryPath.split('/');
|
||||
parts.pop();
|
||||
|
||||
const parentPath = parts.join('/');
|
||||
let parentNode = structure[''];
|
||||
if(parentPath){
|
||||
const parentParts = parentPath.split('/');
|
||||
for (const part of parentParts) {
|
||||
parentNode = parentNode.subdirs[part];
|
||||
}
|
||||
}
|
||||
|
||||
parentNode.files.push(entry);
|
||||
let parentSubdirs = structure[''].subdirs;
|
||||
parts.forEach(part => {
|
||||
if (!parentSubdirs[part]) {
|
||||
parentSubdirs[part] = { files: [], subdirs: {} };
|
||||
}
|
||||
parentSubdirs = parentSubdirs[part].subdirs;
|
||||
});
|
||||
|
||||
return structure;
|
||||
|
||||
const parentPath = parts.join('/');
|
||||
let parentNode = structure[''];
|
||||
if (parentPath) {
|
||||
const parentParts = parentPath.split('/');
|
||||
for (const part of parentParts) {
|
||||
parentNode = parentNode.subdirs[part];
|
||||
}
|
||||
}
|
||||
|
||||
parentNode.files.push(entry);
|
||||
});
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
|
||||
@ -190,19 +190,19 @@ function buildSidebarFromStructure(
|
||||
const rootFiles = structure[''].files
|
||||
.filter(entry => !isPageHidden(entry))
|
||||
.map(entry => createSidebarLink(entry, baseDirectory, locale, currentPath));
|
||||
|
||||
|
||||
// Add root files without sorting (will be sorted at the end)
|
||||
items.push(...rootFiles);
|
||||
}
|
||||
|
||||
|
||||
// Process subdirectories if we haven't reached max depth
|
||||
if (currentDepth < maxDepth) {
|
||||
const subdirs = structure['']?.subdirs || {};
|
||||
const subdirs = structure['']?.subdirs || {};
|
||||
Object.entries(subdirs).forEach(([dirName, dirData]) => {
|
||||
if (dirName === '') return; // Skip root files (already processed)
|
||||
|
||||
|
||||
const subItems: (SidebarLink | SidebarGroup)[] = [];
|
||||
|
||||
|
||||
// Add files in this subdirectory
|
||||
if (dirData.files.length > 0) {
|
||||
const subFiles = dirData.files
|
||||
@ -210,15 +210,15 @@ function buildSidebarFromStructure(
|
||||
.map(entry => createSidebarLink(entry, baseDirectory, locale, currentPath));
|
||||
subItems.push(...subFiles);
|
||||
}
|
||||
|
||||
|
||||
// Recursively add nested subdirectories
|
||||
if (Object.keys(dirData.subdirs).length > 0) {
|
||||
const nestedStructure = {'': {files: [], subdirs: dirData.subdirs}}
|
||||
const nestedStructure = { '': { files: [], subdirs: dirData.subdirs } }
|
||||
const nestedItems = buildSidebarFromStructure(
|
||||
nestedStructure,
|
||||
baseDirectory,
|
||||
currentPath,
|
||||
maxDepth,
|
||||
nestedStructure,
|
||||
baseDirectory,
|
||||
currentPath,
|
||||
maxDepth,
|
||||
currentDepth + 1,
|
||||
collapsedByDefault,
|
||||
sortBy,
|
||||
@ -228,11 +228,11 @@ function buildSidebarFromStructure(
|
||||
);
|
||||
subItems.push(...nestedItems);
|
||||
}
|
||||
|
||||
|
||||
if (subItems.length > 0) {
|
||||
// Sort the subItems before adding to the group
|
||||
const sortedSubItems = applySorting(subItems, sortBy, customSort, entries);
|
||||
|
||||
|
||||
items.push({
|
||||
label: formatDirectoryName(dirName),
|
||||
items: sortedSubItems,
|
||||
@ -242,7 +242,7 @@ function buildSidebarFromStructure(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return applySorting(items, sortBy, customSort, entries);
|
||||
}
|
||||
|
||||
@ -266,18 +266,18 @@ function createSidebarLink(entry: any, baseDirectory: string, locale: string, cu
|
||||
} else if (entry.data.name) {
|
||||
label = entry.data.name;
|
||||
}
|
||||
|
||||
|
||||
// Clean up label if it includes directory path
|
||||
if (label.includes('/')) {
|
||||
label = label.split('/').pop() || label;
|
||||
}
|
||||
|
||||
|
||||
// Remove file extension from entry.id for clean URLs
|
||||
const cleanId = entry.id.replace(/\.(md|mdx)$/, '');
|
||||
|
||||
|
||||
// Generate href with locale if provided
|
||||
const href = locale ? `/${locale}/${baseDirectory}/${cleanId}/` : `/${baseDirectory}/${cleanId}/`;
|
||||
|
||||
|
||||
return {
|
||||
label,
|
||||
href,
|
||||
@ -308,7 +308,7 @@ function sortAlphabetically(a: SidebarLink | SidebarGroup, b: SidebarLink | Side
|
||||
if (!isAFile && isBFile) {
|
||||
return 1; // b (file) comes before a (folder)
|
||||
}
|
||||
|
||||
|
||||
// If both are files or both are folders, sort by label
|
||||
return a.label.localeCompare(b.label);
|
||||
}
|
||||
@ -321,17 +321,17 @@ function sortByDate(a: SidebarLink | SidebarGroup, b: SidebarLink | SidebarGroup
|
||||
if (!('href' in a) || !('href' in b)) {
|
||||
return sortAlphabetically(a, b);
|
||||
}
|
||||
|
||||
|
||||
// Find corresponding entries for date comparison
|
||||
const entryA = entries.find(entry => a.href?.includes(entry.id.replace(/\.(md|mdx)$/, '')));
|
||||
const entryB = entries.find(entry => b.href?.includes(entry.id.replace(/\.(md|mdx)$/, '')));
|
||||
|
||||
|
||||
if (entryA?.data?.pubDate && entryB?.data?.pubDate) {
|
||||
const dateA = new Date(entryA.data.pubDate);
|
||||
const dateB = new Date(entryB.data.pubDate);
|
||||
return dateB.getTime() - dateA.getTime(); // Newest first
|
||||
}
|
||||
|
||||
|
||||
// Fallback to alphabetical if no dates
|
||||
return sortAlphabetically(a, b);
|
||||
}
|
||||
@ -340,7 +340,7 @@ function sortByDate(a: SidebarLink | SidebarGroup, b: SidebarLink | SidebarGroup
|
||||
* Apply sorting to sidebar items
|
||||
*/
|
||||
function applySorting(
|
||||
items: (SidebarLink | SidebarGroup)[],
|
||||
items: (SidebarLink | SidebarGroup)[],
|
||||
sortBy: SortFunction = 'alphabetical',
|
||||
customSort?: (a: SidebarLink | SidebarGroup, b: SidebarLink | SidebarGroup) => number,
|
||||
entries?: any[]
|
||||
@ -371,7 +371,7 @@ export async function processSidebarGroup(group: SidebarGroup, currentPath?: str
|
||||
const maxDepth = group.autogenerate.maxDepth ?? 2;
|
||||
const sortBy = group.autogenerate.sortBy ?? 'alphabetical';
|
||||
const items = await generateLinksFromDirectoryWithConfig(
|
||||
group.autogenerate.directory,
|
||||
group.autogenerate.directory,
|
||||
{
|
||||
currentPath,
|
||||
maxDepth,
|
||||
@ -392,8 +392,8 @@ export async function processSidebarGroup(group: SidebarGroup, currentPath?: str
|
||||
return {
|
||||
...item,
|
||||
href: item.slug ? `/${item.slug}/` : item.href,
|
||||
isCurrent: currentPath ?
|
||||
(item.slug ? currentPath.includes(`/${item.slug}`) : currentPath === item.href) :
|
||||
isCurrent: currentPath ?
|
||||
(item.slug ? currentPath.includes(`/${item.slug}`) : currentPath === item.href) :
|
||||
false,
|
||||
};
|
||||
} else {
|
||||
|
||||
@ -2,22 +2,21 @@
|
||||
import * as path from "path";
|
||||
import { sync as fileExists } from "@polymech/fs/exists";
|
||||
|
||||
import { specs } from "@/base/specs.js";
|
||||
|
||||
import { createComponent } from "astro/runtime/server/astro-component.js";
|
||||
import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
|
||||
|
||||
import { logger } from "@/base/index.js";
|
||||
import { translateSheets } from "@/base/i18n.js";
|
||||
import {
|
||||
I18N_SOURCE_LANGUAGE,
|
||||
LANGUAGES,
|
||||
PRODUCT_SPECS,
|
||||
} from "config/config.js";
|
||||
import { specs } from "../base/specs.js";
|
||||
import { logger } from "../base/index.js";
|
||||
import { translateSheets } from "../base/i18n.js";
|
||||
import { PRODUCT_SPECS } from "../app/config.js";
|
||||
|
||||
import { loadConfig } from "../app/config-loader.js";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
import DefaultComponent from "./Default.astro";
|
||||
const { frontmatter: data } = Astro.props;
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE;
|
||||
const locale = Astro.currentLocale || config.i18n.source_language;
|
||||
|
||||
const specs_table = async (relPath) => {
|
||||
let specsPath = path.join(PRODUCT_SPECS(relPath));
|
||||
@ -25,7 +24,10 @@ const specs_table = async (relPath) => {
|
||||
logger.debug(`No specs found for ${specsPath}`);
|
||||
return null;
|
||||
}
|
||||
if (locale !== I18N_SOURCE_LANGUAGE && LANGUAGES.includes(locale)) {
|
||||
if (
|
||||
locale !== config.i18n.source_language &&
|
||||
config.core.languages.includes(locale)
|
||||
) {
|
||||
const i18n = await translateSheets(relPath, locale);
|
||||
if (!i18n) {
|
||||
logger.debug(`No i18n found for ${relPath} : ${locale}`);
|
||||
|
||||
407
packages/polymech/src/components/store/Checkout.astro
Normal file
407
packages/polymech/src/components/store/Checkout.astro
Normal file
@ -0,0 +1,407 @@
|
||||
---
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: "OP-1 Portable Synthesizer",
|
||||
price: "$1,299.00",
|
||||
description: "All-in-one portable synthesizer, sampler, and controller.",
|
||||
storage: "64MB",
|
||||
imageSrc: "/products/1.jpeg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "PO-33 Pocket Operator",
|
||||
price: "$89.00",
|
||||
description:
|
||||
"Micro sampler and sequencer with built-in microphone and 40-second sample memory.",
|
||||
storage: "4MB",
|
||||
imageSrc: "/products/17.jpeg",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 just xl:grid-cols-3 gap-2">
|
||||
<div class="flex flex-col gap-2 h-full justify-between">
|
||||
<div>
|
||||
<h1
|
||||
class="text-lg text-neutral-600 tracking-tight text-balance">
|
||||
IPHONE 14 PRO MAX
|
||||
</h1>
|
||||
<p class=" mt-2 text-sm">
|
||||
Elevate your iPhone 14 Pro Max's aesthetic with our exclusive
|
||||
collection. Gone are the days of conventional designs. Our innovative
|
||||
process transforms standard patterns into exceptional skins for your
|
||||
device.
|
||||
</p>
|
||||
<form class="bg-white p-4 mt-6 rounded-xl">
|
||||
<div class="grid grid-cols-12 gap-1">
|
||||
<div class="col-span-full">
|
||||
<label
|
||||
for="email-address"
|
||||
class="sr-only"
|
||||
>Email address</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="email"
|
||||
id="email-address"
|
||||
name="email-address"
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autocomplete="email"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label
|
||||
for="name-on-card"
|
||||
class="sr-only"
|
||||
>Name on card</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
id="name-on-card"
|
||||
name="name-on-card"
|
||||
placeholder="Name on card"
|
||||
autocomplete="cc-name"
|
||||
aria-label="Name on card"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label
|
||||
for="card-number"
|
||||
class="sr-only"
|
||||
>Card number</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
id="card-number"
|
||||
name="card-number"
|
||||
autocomplete="cc-number"
|
||||
placeholder="Card number"
|
||||
aria-label="Card number"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-8 sm:col-span-9">
|
||||
<label
|
||||
for="expiration-date"
|
||||
class="sr-only"
|
||||
>Expiration date (MM/YY)</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="expiration-date"
|
||||
id="expiration-date"
|
||||
placeholder="Expiration date (MM/YY)"
|
||||
aria-label="Expiration date"
|
||||
autocomplete="cc-exp"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-4 sm:col-span-3">
|
||||
<label
|
||||
for="cvc"
|
||||
class="sr-only"
|
||||
>CVC</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
id="cvc"
|
||||
type="text"
|
||||
name="cvc"
|
||||
placeholder="CVC"
|
||||
autocomplete="cc-csc"
|
||||
aria-label="CVC"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label
|
||||
for="address"
|
||||
class="sr-only"
|
||||
>Address</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
id="address"
|
||||
type="text"
|
||||
name="address"
|
||||
placeholder="Address"
|
||||
aria-label="Address"
|
||||
autocomplete="street-address"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full sm:col-span-4">
|
||||
<label
|
||||
for="city"
|
||||
class="sr-only"
|
||||
>City</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
id="city"
|
||||
type="text"
|
||||
name="city"
|
||||
placeholder="City"
|
||||
aria-label="City"
|
||||
autocomplete="address-level2"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full sm:col-span-4">
|
||||
<label
|
||||
for="region"
|
||||
class="sr-only"
|
||||
>State / Province</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
id="region"
|
||||
type="text"
|
||||
name="region"
|
||||
placeholder="State / Province"
|
||||
aria-label="State / Province"
|
||||
autocomplete="address-level1"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full sm:col-span-4">
|
||||
<label
|
||||
for="postal-code"
|
||||
class="sr-only"
|
||||
>Postal code</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
id="postal-code"
|
||||
type="text"
|
||||
name="postal-code"
|
||||
placeholder="Postal Code"
|
||||
autocomplete="postal-code"
|
||||
aria-label="Postal code"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full mt-2">
|
||||
<label
|
||||
for="discount-code"
|
||||
class="sr-only"
|
||||
>Discount code</label
|
||||
>
|
||||
<div class="mt-1 flex w-full space-x-2">
|
||||
<input
|
||||
id="discount-code"
|
||||
type="text"
|
||||
name="discount-code"
|
||||
placeholder="Discount Code"
|
||||
aria-label="Discount code"
|
||||
class="flex-auto rounded-xl border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 ring-1 ring-inset ring-white placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 bg-neutral-100 w-full"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden px-6 justify-center text-xs h-14 flex uppercase items-center bg-black hover:bg-neutral-200 hover:text-orange-600 duration-300 rounded-xl w-full">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 justify-between text-xs h-14 flex space-x-6 items-center bg-black hover:bg-neutral-200 hover:text-orange-600 duration-300 rounded-xl w-full">
|
||||
<span class="relative uppercase text-xs">Pay with apple</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7">
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center hover:bg-black duration-300 rounded-xl w-full justify-between">
|
||||
<span class="relative uppercase text-xs ">Pay</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7">
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-2 space-y-2">
|
||||
<div class="">
|
||||
<ul
|
||||
role="list"
|
||||
class="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||
{
|
||||
products.map((product) => (
|
||||
<li
|
||||
x-data="{ show: true }"
|
||||
x-show="show">
|
||||
<div class="flex flex-col gap-2 relative p-4 bg-white rounded-xl">
|
||||
<img
|
||||
src={product.imageSrc}
|
||||
alt={product.name}
|
||||
class="aspect-[4/2] flex-none rounded-lg bg-neutral-200 object-cover object-center"
|
||||
/>
|
||||
<div class="flex flex-col justify-between w-full">
|
||||
<div class=" text-sm ">
|
||||
<h3 class="text-lg text-neutral-600 uppercase tracking-tight">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p class=" ">
|
||||
{product.price} — {product.storage}
|
||||
</p>
|
||||
<p class=" text-balance mt-8">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex ml-auto mt-16">
|
||||
<button
|
||||
@click="show = false"
|
||||
type="submit"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group px-6 justify-center text-xs text-orange-600 uppercase h-8 flex items-center bg-neutral-100 hover:bg-neutral-200 hover:text-orange-600 duration-300 rounded-lg w-full">
|
||||
<span class="sr-only">Remove</span>
|
||||
Remove
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded-xl">
|
||||
<div class="flex-none pt-2">
|
||||
<dl class="space-y-6 text-sm ">
|
||||
<div class="flex justify-between">
|
||||
<dt
|
||||
class="text-lg text-neutral-600 uppercase tracking-tight">
|
||||
Subtotal
|
||||
</dt>
|
||||
<dd class=" ">$920.00</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="flex">
|
||||
<span
|
||||
class="text-lg text-neutral-600 uppercase tracking-tight"
|
||||
>Discount</span
|
||||
>
|
||||
<span
|
||||
class="ml-2 rounded-md bg-neutral-100 px-2 py-0.5 inline-flex items-center text-xs tracking-wide text-orange-600 uppercase"
|
||||
>LEXINGTON30</span
|
||||
>
|
||||
</dt>
|
||||
<dd class=" ">-$30.00</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt
|
||||
class="text-lg text-neutral-600 uppercase tracking-tight">
|
||||
Taxes
|
||||
</dt>
|
||||
<dd class=" ">$403.68</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt
|
||||
class="text-lg text-neutral-600 uppercase tracking-tight">
|
||||
Shipping
|
||||
</dt>
|
||||
<dd class=" ">Free</dd>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-neutral-200 pt-6 ">
|
||||
<dt class="text-base">Total</dt>
|
||||
<dd class="text-base">$3.019</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
47
packages/polymech/src/components/store/StoreEntries.astro
Normal file
47
packages/polymech/src/components/store/StoreEntries.astro
Normal file
@ -0,0 +1,47 @@
|
||||
---
|
||||
import { default_image } from "@/app/config.js";
|
||||
const { title, url, price, model } = Astro.props;
|
||||
const thumbnail =
|
||||
model?.assets?.main_image?.url ||
|
||||
model?.assets?.gallery[0]?.url ||
|
||||
default_image();
|
||||
|
||||
const hash = model?.assets?.main_image?.hash || model?.assets?.gallery[0]?.hash;
|
||||
|
||||
import Img from "@polymech/astro-base/components/polymech/image.astro";
|
||||
---
|
||||
|
||||
<div
|
||||
class="group relative overflow-hidden rounded-xl shadow-sm hover:shadow-md transition-shadow duration-300"
|
||||
>
|
||||
<div class="aspect-square overflow-hidden">
|
||||
<a href={url} title={title} aria-label={title} class="block w-full h-full">
|
||||
<div class="w-full h-full flex items-center justify-center p-0 md:p-1">
|
||||
<Img
|
||||
src={thumbnail}
|
||||
s={hash}
|
||||
alt={title}
|
||||
format="avif"
|
||||
objectFit="contain"
|
||||
placeholder="blurred"
|
||||
sizes="(min-width: 220px) 220px"
|
||||
breakpoints={{ count: 2, minWidth: 120, maxWidth: 430 }}
|
||||
class="max-w-full max-h-full object-contain group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-medium line-clamp-2">
|
||||
<a
|
||||
href={url}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
class="hover:text-neutral-900 transition-colors"
|
||||
>
|
||||
<span aria-hidden="true" class="absolute inset-0"></span>
|
||||
{title}
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,7 +1,6 @@
|
||||
---
|
||||
import { translate } from "@/base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { slugify } from "@/base/strings.js"
|
||||
import { translate } from "../base/i18n.js";
|
||||
import { slugify } from "../base/strings.js";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@ -12,7 +11,7 @@ const slug = `${slugify(title)}-tab-button`;
|
||||
const id = `#${slug}-tab`;
|
||||
const view = `#${slugify(title)}-view`;
|
||||
const locale = Astro.currentLocale;
|
||||
const title_i18n = await translate(title, I18N_SOURCE_LANGUAGE, locale);
|
||||
const title_i18n = await translate(title, "en", locale);
|
||||
---
|
||||
|
||||
<li>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
import { slugify } from "@/base/strings.js";
|
||||
import { slugify } from "../base/strings.js";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
---
|
||||
import { translate } from '@/base/i18n.js';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { translate } from "../base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "../app/config.js";
|
||||
|
||||
// A simple slugify helper (you could import this from a utility file instead)
|
||||
function slugify(text: string) {
|
||||
return text
|
||||
.normalize('NFD') // Normalise accented characters
|
||||
.replace(/[\u0300-\u036f]/g, '') // Strip accents
|
||||
.normalize("NFD") // Normalise accented characters
|
||||
.replace(/[\u0300-\u036f]/g, "") // Strip accents
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
||||
.replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
|
||||
.replace(/[^a-z0-9]+/g, "-") // Replace non-alphanumeric with hyphens
|
||||
.replace(/^-+|-+$/g, ""); // Trim leading/trailing hyphens
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
@ -18,11 +18,7 @@ export interface Props {
|
||||
src?: string; // optional
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'title',
|
||||
clazz = 'unstyled',
|
||||
src,
|
||||
} = Astro.props;
|
||||
const { title = "title", clazz = "unstyled", src } = Astro.props;
|
||||
|
||||
// Compute the slug from the original title
|
||||
const id = slugify(title);
|
||||
@ -36,16 +32,19 @@ const title_i18n = await translate(title, I18N_SOURCE_LANGUAGE, locale);
|
||||
id={id}
|
||||
slot="tab"
|
||||
class="inline-flex whitespace-nowrap border-b-2 border-transparent py-2 px-3 text-sm font-medium text-gray-300 transition-all duration-200 ease-in-out hover:border-b-[#9249ed] hover:text-[#9249ed] aria-selected:border-b-[#9249ed] aria-selected:text-[#9249ed]"
|
||||
>{title_i18n}</button>
|
||||
>{title_i18n}</button
|
||||
>
|
||||
|
||||
<div slot="panel" class={clazz}>
|
||||
{src ? (
|
||||
<div class="iframe-container">
|
||||
<iframe src={src}></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<slot />
|
||||
)}
|
||||
{
|
||||
src ? (
|
||||
<div class="iframe-container">
|
||||
<iframe src={src} />
|
||||
</div>
|
||||
) : (
|
||||
<slot />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
1
packages/polymech/src/env.d.ts
vendored
Normal file
1
packages/polymech/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
@ -2,16 +2,22 @@
|
||||
import BaseHead from "../components/BaseHead.astro";
|
||||
import Navigation from "../components/global/Navigation.astro";
|
||||
import Footer from "../components/global/Footer.astro";
|
||||
import { isRTL } from "config/config.js"
|
||||
import { isRTL } from "../app/config.js";
|
||||
|
||||
const { frontmatter: frontmatter, ...rest } = Astro.props;
|
||||
|
||||
---
|
||||
<html lang={Astro.currentLocale} class="scroll-smooth" dir={isRTL(Astro.currentLocale) ? "rtl" : "ltr"}>
|
||||
|
||||
<html
|
||||
lang={Astro.currentLocale}
|
||||
class="scroll-smooth"
|
||||
dir={isRTL(Astro.currentLocale) ? "rtl" : "ltr"}
|
||||
>
|
||||
<head>
|
||||
<BaseHead frontmatter={frontmatter} {...rest} />
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 mx-auto 2xl:max-w-7xl flex flex-col min-h-svh p-4 transition-colors duration-200">
|
||||
<body
|
||||
class="bg-white dark:bg-gray-900 mx-auto 2xl:max-w-7xl flex flex-col min-h-svh p-4 transition-colors duration-200"
|
||||
>
|
||||
<Navigation />
|
||||
<main class="grow"><slot /></main>
|
||||
<Footer />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
/**
|
||||
* Resources Layout
|
||||
*
|
||||
*
|
||||
* Features:
|
||||
* - Automatic top-level Table of Contents for articles with 20+ headings
|
||||
* - Sidebar navigation with page-level navigation support
|
||||
@ -9,18 +9,17 @@
|
||||
* - Image lightbox functionality
|
||||
* - Responsive design with mobile sidebar toggle
|
||||
*/
|
||||
import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro"
|
||||
import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro"
|
||||
import Breadcrumb from "@polymech/astro-base/components/Breadcrumb.astro";
|
||||
import Translate from "@polymech/astro-base/components/i18n.astro";
|
||||
import { getSidebarConfig } from '@polymech/astro-base/config/sidebar';
|
||||
import { generateToC } from '@polymech/astro-base/components/sidebar/utils/generateToC.js';
|
||||
import Sidebar from "../components/sidebar/Sidebar.astro";
|
||||
import MobileToggle from "../components/sidebar/MobileToggle.astro";
|
||||
import Breadcrumb from "../components/Breadcrumb.astro";
|
||||
import Translate from "../components/i18n.astro";
|
||||
import { getSidebarConfig } from "../config/sidebar";
|
||||
import { generateToC } from "../components/sidebar/utils/generateToC.js";
|
||||
|
||||
import RelativeImage from '@polymech/astro-base/components/RelativeImage.astro';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
import BaseLayout from "./BaseLayout.astro"
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
|
||||
interface Props {
|
||||
frontmatter: {
|
||||
@ -35,7 +34,7 @@ interface Props {
|
||||
};
|
||||
sidebar?: any; // Page-level sidebar configuration
|
||||
breadcrumb?: boolean; // Enable/disable breadcrumb (default: true)
|
||||
bottomNav?: 'PREV/NEXT' | 'TOP' | false; // Bottom navigation type
|
||||
bottomNav?: "PREV/NEXT" | "TOP" | false; // Bottom navigation type
|
||||
minutesRead?: string; // Reading time from remark plugin
|
||||
};
|
||||
headings?: MarkdownHeading[];
|
||||
@ -43,34 +42,42 @@ interface Props {
|
||||
collectionName?: string; // Collection name for dynamic functionality
|
||||
}
|
||||
|
||||
const { frontmatter, headings = [], entryPath, collectionName = 'resources' } = Astro.props; // Updated to get entryPath and collectionName
|
||||
const {
|
||||
frontmatter,
|
||||
headings = [],
|
||||
entryPath,
|
||||
collectionName = "resources",
|
||||
} = Astro.props; // Updated to get entryPath and collectionName
|
||||
const locale = Astro.currentLocale;
|
||||
const sidebarConfig = getSidebarConfig();
|
||||
|
||||
|
||||
// Extract page-level sidebar configuration from frontmatter
|
||||
const pageNavigation = frontmatter.sidebar ? [frontmatter.sidebar].flat() : [];
|
||||
|
||||
// Format the date
|
||||
const formattedDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}) : '';
|
||||
const formattedDate = frontmatter.pubDate
|
||||
? new Date(frontmatter.pubDate).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: "";
|
||||
|
||||
// Generate top-level TOC for articles with many headings (10+)
|
||||
const shouldShowTopToc = headings.length > 10;
|
||||
const topToc = shouldShowTopToc ? generateToC(headings, {
|
||||
minHeadingLevel: 2,
|
||||
maxHeadingLevel: 3,
|
||||
title: 'Table of Contents'
|
||||
}) : null;
|
||||
const topToc = shouldShowTopToc
|
||||
? generateToC(headings, {
|
||||
minHeadingLevel: 2,
|
||||
maxHeadingLevel: 3,
|
||||
title: "Table of Contents",
|
||||
})
|
||||
: null;
|
||||
|
||||
// Bottom navigation configuration
|
||||
const BOTTOM_NAV = frontmatter.bottomNav || 'PREV/NEXT';
|
||||
const BOTTOM_NAV = frontmatter.bottomNav || "PREV/NEXT";
|
||||
const showBottomNav = BOTTOM_NAV !== false;
|
||||
const isPrevNext = BOTTOM_NAV === 'PREV/NEXT';
|
||||
const isTop = BOTTOM_NAV === 'TOP';
|
||||
const isPrevNext = BOTTOM_NAV === "PREV/NEXT";
|
||||
const isTop = BOTTOM_NAV === "TOP";
|
||||
|
||||
// Get previous and next articles for navigation
|
||||
let prevArticle: any = null;
|
||||
@ -80,29 +87,31 @@ if (isPrevNext && entryPath) {
|
||||
try {
|
||||
// Get all articles from the collection
|
||||
const allArticles = await getCollection(collectionName);
|
||||
|
||||
|
||||
// Extract the current article path without the collection prefix
|
||||
const currentArticlePath = entryPath.replace(`${collectionName}/`, '');
|
||||
|
||||
const currentArticlePath = entryPath.replace(`${collectionName}/`, "");
|
||||
|
||||
// Sort articles by their full path to maintain hierarchical order
|
||||
const sortedArticles = allArticles.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
|
||||
// Find current article index
|
||||
const currentIndex = sortedArticles.findIndex(article => article.id === currentArticlePath);
|
||||
|
||||
const currentIndex = sortedArticles.findIndex(
|
||||
(article) => article.id === currentArticlePath,
|
||||
);
|
||||
|
||||
if (currentIndex !== -1) {
|
||||
// Get previous article
|
||||
if (currentIndex > 0) {
|
||||
prevArticle = sortedArticles[currentIndex - 1];
|
||||
}
|
||||
|
||||
|
||||
// Get next article
|
||||
if (currentIndex < sortedArticles.length - 1) {
|
||||
nextArticle = sortedArticles[currentIndex + 1];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch collection for navigation:', error);
|
||||
console.warn("Could not fetch collection for navigation:", error);
|
||||
}
|
||||
}
|
||||
---
|
||||
@ -111,208 +120,313 @@ if (isPrevNext && entryPath) {
|
||||
<div class="layout-with-sidebar">
|
||||
<!-- Mobile Toggle -->
|
||||
<MobileToggle />
|
||||
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar-wrapper">
|
||||
<Sidebar
|
||||
config={sidebarConfig}
|
||||
currentUrl={Astro.url}
|
||||
headings={headings}
|
||||
<Sidebar
|
||||
config={sidebarConfig}
|
||||
currentUrl={Astro.url}
|
||||
headings={headings}
|
||||
pageNavigation={pageNavigation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content-with-sidebar">
|
||||
<div class="px-4 py-4 md:px-6 md:py-6">
|
||||
{/* Breadcrumb - enabled by default, can be disabled with breadcrumb: false */}
|
||||
{frontmatter.breadcrumb !== false && (
|
||||
<Breadcrumb
|
||||
currentPath={Astro.url.pathname}
|
||||
collection={collectionName}
|
||||
title={frontmatter.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
<article class="prose prose-lg max-w-full md:max-w-4xl mx-auto overflow-x-hidden" id="top">
|
||||
{
|
||||
/* Breadcrumb - enabled by default, can be disabled with breadcrumb: false */
|
||||
}
|
||||
{
|
||||
frontmatter.breadcrumb !== false && (
|
||||
<Breadcrumb
|
||||
currentPath={Astro.url.pathname}
|
||||
collection={collectionName}
|
||||
title={frontmatter.title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<article
|
||||
class="prose prose-lg max-w-full md:max-w-4xl mx-auto overflow-x-hidden"
|
||||
id="top"
|
||||
>
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||
{frontmatter.title}
|
||||
</h1>
|
||||
{/* Reading time and metadata */}
|
||||
{(frontmatter.author && frontmatter.author.toLowerCase() !== 'unknown') || formattedDate || frontmatter.minutesRead ? (
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-600 mb-4">
|
||||
{frontmatter.minutesRead && (
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<Translate>{frontmatter.minutesRead}</Translate>
|
||||
</span>
|
||||
)}
|
||||
{frontmatter.author && frontmatter.author.toLowerCase() !== 'unknown' && <span>By {frontmatter.author}</span>}
|
||||
{formattedDate && <span>{formattedDate}</span>}
|
||||
</div>
|
||||
) : null}
|
||||
<p class="text-xl text-gray-700 mb-6">{frontmatter.description}</p>
|
||||
{frontmatter.tags && frontmatter.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
{frontmatter.tags.map((tag: string) => (
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/tags/${tag}`}
|
||||
class="px-3 py-1 text-sm bg-blue-100 text-blue-800 rounded-full hover:bg-blue-200 hover:text-blue-900 transition-colors cursor-pointer"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top-level Table of Contents for long articles */}
|
||||
{shouldShowTopToc && topToc && (
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
<Translate>Table of Contents</Translate>
|
||||
</h2>
|
||||
<nav class="top-toc-nav">
|
||||
{topToc.length >= 10 ? (
|
||||
/* 2-column layout for 10+ items with even distribution */
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2">
|
||||
{/* Debug info */}
|
||||
|
||||
{/* Left column: first half of items */}
|
||||
<div class="space-y-2">
|
||||
{topToc.slice(0, Math.floor(topToc.length / 2)).map((item, index) => (
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline transition-colors block py-1 rounded px-2 hover:bg-blue-50 leading-relaxed"
|
||||
>
|
||||
<span class="font-mono text-gray-500 mr-2 flex-shrink-0">{index + 1}.</span>
|
||||
<span class="toc-text">{item.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Right column: second half of items */}
|
||||
<div class="space-y-2">
|
||||
{topToc.slice(Math.floor(topToc.length / 2)).map((item, index) => (
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline transition-colors block py-1 rounded px-2 hover:bg-blue-50 leading-relaxed"
|
||||
>
|
||||
<span class="font-mono text-gray-500 mr-2 flex-shrink-0">{Math.floor(topToc.length / 2) + index + 1}.</span>
|
||||
<span class="toc-text">{item.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Single column for fewer items */
|
||||
<ul class="space-y-2">
|
||||
{topToc.map((item, index) => (
|
||||
<li>
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline transition-colors block py-1 leading-relaxed"
|
||||
>
|
||||
<span class="font-mono text-gray-500 mr-2 flex-shrink-0">{index + 1}.</span>
|
||||
<span class="inline-block">{item.text}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{
|
||||
(frontmatter.author &&
|
||||
frontmatter.author.toLowerCase() !== "unknown") ||
|
||||
formattedDate ||
|
||||
frontmatter.minutesRead ? (
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-600 mb-4">
|
||||
{frontmatter.minutesRead && (
|
||||
<span class="flex items-center">
|
||||
<svg
|
||||
class="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Translate>{frontmatter.minutesRead}</Translate>
|
||||
</span>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
{frontmatter.author &&
|
||||
frontmatter.author.toLowerCase() !== "unknown" && (
|
||||
<span>By {frontmatter.author}</span>
|
||||
)}
|
||||
{formattedDate && <span>{formattedDate}</span>}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<p class="text-xl text-gray-700 mb-6">{frontmatter.description}</p>
|
||||
{
|
||||
frontmatter.tags && frontmatter.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
{frontmatter.tags.map((tag: string) => (
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/tags/${tag}`}
|
||||
class="px-3 py-1 text-sm bg-blue-100 text-blue-800 rounded-full hover:bg-blue-200 hover:text-blue-900 transition-colors cursor-pointer"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Top-level Table of Contents for long articles */}
|
||||
{
|
||||
shouldShowTopToc && topToc && (
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
<Translate>Table of Contents</Translate>
|
||||
</h2>
|
||||
<nav class="top-toc-nav">
|
||||
{topToc.length >= 10 ? (
|
||||
/* 2-column layout for 10+ items with even distribution */
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2">
|
||||
{/* Debug info */}
|
||||
|
||||
{/* Left column: first half of items */}
|
||||
<div class="space-y-2">
|
||||
{topToc
|
||||
.slice(0, Math.floor(topToc.length / 2))
|
||||
.map((item, index) => (
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline transition-colors block py-1 rounded px-2 hover:bg-blue-50 leading-relaxed"
|
||||
>
|
||||
<span class="font-mono text-gray-500 mr-2 flex-shrink-0">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<span class="toc-text">{item.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Right column: second half of items */}
|
||||
<div class="space-y-2">
|
||||
{topToc
|
||||
.slice(Math.floor(topToc.length / 2))
|
||||
.map((item, index) => (
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline transition-colors block py-1 rounded px-2 hover:bg-blue-50 leading-relaxed"
|
||||
>
|
||||
<span class="font-mono text-gray-500 mr-2 flex-shrink-0">
|
||||
{Math.floor(topToc.length / 2) + index + 1}.
|
||||
</span>
|
||||
<span class="toc-text">{item.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Single column for fewer items */
|
||||
<ul class="space-y-2">
|
||||
{topToc.map((item, index) => (
|
||||
<li>
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline transition-colors block py-1 leading-relaxed"
|
||||
>
|
||||
<span class="font-mono text-gray-500 mr-2 flex-shrink-0">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<span class="inline-block">{item.text}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="markdown-content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Tags at bottom of article */}
|
||||
{frontmatter.tags && frontmatter.tags.length > 0 && (
|
||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">Tags:</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{frontmatter.tags.map((tag: string) => (
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/tags/${tag}`}
|
||||
class="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 hover:text-gray-900 transition-colors cursor-pointer"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
{
|
||||
frontmatter.tags && frontmatter.tags.length > 0 && (
|
||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">Tags:</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{frontmatter.tags.map((tag: string) => (
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/tags/${tag}`}
|
||||
class="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 hover:text-gray-900 transition-colors cursor-pointer"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
{showBottomNav && (
|
||||
<div class="mt-12 pt-8 border-t border-gray-200">
|
||||
{isPrevNext ? (
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex-1">
|
||||
{prevArticle ? (
|
||||
<a href={`/${locale}/${collectionName}/${prevArticle.id}`} class="group inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">← {prevArticle.data.title}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="text-gray-400 text-sm">
|
||||
<Translate>First Article</Translate> ←
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
showBottomNav && (
|
||||
<div class="mt-12 pt-8 border-t border-gray-200">
|
||||
{isPrevNext ? (
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex-1">
|
||||
{prevArticle ? (
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/${prevArticle.id}`}
|
||||
class="group inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">
|
||||
← {prevArticle.data.title}
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="text-gray-400 text-sm">
|
||||
<Translate>First Article</Translate> ←
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex-1 text-right">
|
||||
{nextArticle ? (
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/${nextArticle.id}`}
|
||||
class="group inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<span class="text-sm font-medium">
|
||||
{nextArticle.data.title} →
|
||||
</span>
|
||||
<svg
|
||||
class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<div class="text-gray-400 text-sm">
|
||||
<Translate>Last Article</Translate> →
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 text-right">
|
||||
{nextArticle ? (
|
||||
<a href={`/${locale}/${collectionName}/${nextArticle.id}`} class="group inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors">
|
||||
<span class="text-sm font-medium">{nextArticle.data.title} →</span>
|
||||
<svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<div class="text-gray-400 text-sm">
|
||||
<Translate>Last Article</Translate> →
|
||||
</div>
|
||||
)}
|
||||
) : isTop ? (
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="#top"
|
||||
class="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">
|
||||
<Translate>Back to Top</Translate>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : isTop ? (
|
||||
<div class="text-center">
|
||||
<a href="#top" class="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">
|
||||
<Translate>Back to Top</Translate>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Global Lightbox -->
|
||||
<div id="global-lightbox" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" style="display: none;">
|
||||
<div
|
||||
id="global-lightbox"
|
||||
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
|
||||
style="display: none;"
|
||||
>
|
||||
<div class="relative max-w-full max-h-full">
|
||||
<img id="lightbox-image" src="" alt="" class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg" />
|
||||
<img
|
||||
id="lightbox-image"
|
||||
src=""
|
||||
alt=""
|
||||
class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
||||
/>
|
||||
|
||||
<!-- Close Button -->
|
||||
<button id="lightbox-close" class="absolute -top-4 -right-4 text-2xl p-2 bg-gray-800/75 rounded-full">×</button>
|
||||
<button
|
||||
id="lightbox-close"
|
||||
class="absolute -top-4 -right-4 text-2xl p-2 bg-gray-800/75 rounded-full"
|
||||
>×</button
|
||||
>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<button id="lightbox-prev" class="absolute left-0 top-1/2 -translate-y-1/2 p-4 text-3xl bg-gray-800/75 rounded-lg">❮</button>
|
||||
<button id="lightbox-next" class="absolute right-0 top-1/2 -translate-y-1/2 p-4 text-3xl bg-gray-800/75 rounded-lg">❯</button>
|
||||
<button
|
||||
id="lightbox-prev"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 p-4 text-3xl bg-gray-800/75 rounded-lg"
|
||||
>❮</button
|
||||
>
|
||||
<button
|
||||
id="lightbox-next"
|
||||
class="absolute right-0 top-1/2 -translate-y-1/2 p-4 text-3xl bg-gray-800/75 rounded-lg"
|
||||
>❯</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@ -324,11 +438,11 @@ if (isPrevNext && entryPath) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
@ -336,29 +450,29 @@ if (isPrevNext && entryPath) {
|
||||
transition: all 0.2s ease;
|
||||
color: #3b82f6; /* text-blue-600 */
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a:hover {
|
||||
background-color: #f3f4f6;
|
||||
color: #1d4ed8; /* text-blue-800 */
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a:visited {
|
||||
color: #8b5cf6; /* text-violet-600 */
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a:visited:hover {
|
||||
color: #7c3aed; /* text-violet-700 */
|
||||
}
|
||||
|
||||
|
||||
/* Visited links styling */
|
||||
.top-toc-nav a.visited {
|
||||
color: #8b5cf6; /* text-violet-600 */
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a.visited:hover {
|
||||
color: #7c3aed; /* text-violet-700 */
|
||||
}
|
||||
|
||||
|
||||
/* Active/current section styling */
|
||||
.top-toc-nav a.active {
|
||||
background-color: #eff6ff; /* bg-blue-50 */
|
||||
@ -367,25 +481,25 @@ if (isPrevNext && entryPath) {
|
||||
border-left: 3px solid #3b82f6; /* border-blue-500 */
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
/* 2-column grid styling */
|
||||
.top-toc-nav .grid {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
|
||||
/* CSS Columns for natural left-to-right flow */
|
||||
.toc-columns {
|
||||
column-count: 2;
|
||||
column-gap: 2rem;
|
||||
column-fill: auto;
|
||||
}
|
||||
|
||||
|
||||
.toc-columns a {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* Better TOC text wrapping */
|
||||
.toc-text {
|
||||
display: inline;
|
||||
@ -393,38 +507,38 @@ if (isPrevNext && entryPath) {
|
||||
hyphens: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
/* Prevent awkward line breaks */
|
||||
.top-toc-nav a {
|
||||
min-height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a .font-mono {
|
||||
margin-top: 0;
|
||||
line-height: 1.4;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.top-toc-nav a .toc-text {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.top-toc-nav .grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.toc-columns {
|
||||
column-count: 1;
|
||||
column-gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Reading time styling */
|
||||
.reading-time {
|
||||
display: inline-flex;
|
||||
@ -433,160 +547,181 @@ if (isPrevNext && entryPath) {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.reading-time svg {
|
||||
color: #9ca3af; /* text-gray-400 */
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
/* Bottom navigation styling */
|
||||
.bottom-nav {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 2rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
|
||||
.bottom-nav a {
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.bottom-nav a:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
|
||||
.bottom-nav svg {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
.bottom-nav .group:hover svg {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
|
||||
.bottom-nav .group:hover svg:last-child {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// TOC functionality
|
||||
const tocLinks = document.querySelectorAll('.top-toc-nav a[href^="#"]');
|
||||
|
||||
|
||||
// Track visited sections
|
||||
tocLinks.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
const targetId = link.getAttribute('href')?.substring(1);
|
||||
tocLinks.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
const targetId = link.getAttribute("href")?.substring(1);
|
||||
if (targetId) {
|
||||
// Mark as visited in localStorage
|
||||
const visited = JSON.parse(localStorage.getItem('toc-visited') || '[]');
|
||||
const visited = JSON.parse(
|
||||
localStorage.getItem("toc-visited") || "[]",
|
||||
);
|
||||
if (!visited.includes(targetId)) {
|
||||
visited.push(targetId);
|
||||
localStorage.setItem('toc-visited', JSON.stringify(visited));
|
||||
localStorage.setItem("toc-visited", JSON.stringify(visited));
|
||||
}
|
||||
|
||||
|
||||
// Update visited state
|
||||
updateTocStates();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Update TOC states based on visited sections and current position
|
||||
function updateTocStates() {
|
||||
const visited = JSON.parse(localStorage.getItem('toc-visited') || '[]');
|
||||
|
||||
tocLinks.forEach(link => {
|
||||
const targetId = link.getAttribute('href')?.substring(1);
|
||||
const visited = JSON.parse(localStorage.getItem("toc-visited") || "[]");
|
||||
|
||||
tocLinks.forEach((link) => {
|
||||
const targetId = link.getAttribute("href")?.substring(1);
|
||||
if (targetId) {
|
||||
// Remove existing classes
|
||||
link.classList.remove('active', 'visited');
|
||||
|
||||
link.classList.remove("active", "visited");
|
||||
|
||||
// Add visited class if section has been visited
|
||||
if (visited.includes(targetId)) {
|
||||
link.classList.add('visited');
|
||||
link.classList.add("visited");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Intersection Observer for active section highlighting
|
||||
const headings = document.querySelectorAll('h1[id], h2[id], h3[id]');
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.id;
|
||||
|
||||
// Update active state
|
||||
tocLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === `#${id}`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '-20% 0% -35% 0%',
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
headings.forEach(heading => observer.observe(heading));
|
||||
|
||||
const headings = document.querySelectorAll("h1[id], h2[id], h3[id]");
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.id;
|
||||
|
||||
// Update active state
|
||||
tocLinks.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
if (link.getAttribute("href") === `#${id}`) {
|
||||
link.classList.add("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0% -35% 0%",
|
||||
threshold: 0.1,
|
||||
},
|
||||
);
|
||||
|
||||
headings.forEach((heading) => observer.observe(heading));
|
||||
|
||||
// Initialize TOC states
|
||||
updateTocStates();
|
||||
|
||||
|
||||
// Lightbox functionality
|
||||
const lightbox = document.getElementById('global-lightbox') as HTMLElement;
|
||||
const lightboxImage = document.getElementById('lightbox-image') as HTMLImageElement;
|
||||
const closeButton = document.getElementById('lightbox-close') as HTMLButtonElement;
|
||||
const prevButton = document.getElementById('lightbox-prev') as HTMLButtonElement;
|
||||
const nextButton = document.getElementById('lightbox-next') as HTMLButtonElement;
|
||||
const lightbox = document.getElementById("global-lightbox") as HTMLElement;
|
||||
const lightboxImage = document.getElementById(
|
||||
"lightbox-image",
|
||||
) as HTMLImageElement;
|
||||
const closeButton = document.getElementById(
|
||||
"lightbox-close",
|
||||
) as HTMLButtonElement;
|
||||
const prevButton = document.getElementById(
|
||||
"lightbox-prev",
|
||||
) as HTMLButtonElement;
|
||||
const nextButton = document.getElementById(
|
||||
"lightbox-next",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
if (!lightbox || !lightboxImage || !closeButton || !prevButton || !nextButton) return;
|
||||
if (
|
||||
!lightbox ||
|
||||
!lightboxImage ||
|
||||
!closeButton ||
|
||||
!prevButton ||
|
||||
!nextButton
|
||||
)
|
||||
return;
|
||||
|
||||
let images: { src: string, alt: string }[] = [];
|
||||
let images: { src: string; alt: string }[] = [];
|
||||
let currentIndex = 0;
|
||||
|
||||
function updateLightbox() {
|
||||
if (images.length > 0 && images[currentIndex]) {
|
||||
lightboxImage.src = images[currentIndex].src;
|
||||
lightboxImage.alt = images[currentIndex].alt;
|
||||
prevButton.style.display = currentIndex > 0 ? 'block' : 'none';
|
||||
nextButton.style.display = currentIndex < images.length - 1 ? 'block' : 'none';
|
||||
prevButton.style.display = currentIndex > 0 ? "block" : "none";
|
||||
nextButton.style.display =
|
||||
currentIndex < images.length - 1 ? "block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('open-lightbox', (event: CustomEvent) => {
|
||||
window.addEventListener("open-lightbox", (event: CustomEvent) => {
|
||||
images = event.detail.images;
|
||||
currentIndex = event.detail.currentIndex;
|
||||
updateLightbox();
|
||||
lightbox.style.display = 'flex';
|
||||
lightbox.style.display = "flex";
|
||||
});
|
||||
|
||||
closeButton.addEventListener('click', () => {
|
||||
lightbox.style.display = 'none';
|
||||
closeButton.addEventListener("click", () => {
|
||||
lightbox.style.display = "none";
|
||||
});
|
||||
|
||||
prevButton.addEventListener('click', () => {
|
||||
prevButton.addEventListener("click", () => {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
updateLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
nextButton.addEventListener('click', () => {
|
||||
nextButton.addEventListener("click", () => {
|
||||
if (currentIndex < images.length - 1) {
|
||||
currentIndex++;
|
||||
updateLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (lightbox.style.display === 'none') return;
|
||||
if (event.key === 'Escape') closeButton.click();
|
||||
if (event.key === 'ArrowLeft') prevButton.click();
|
||||
if (event.key === 'ArrowRight') nextButton.click();
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (lightbox.style.display === "none") return;
|
||||
if (event.key === "Escape") closeButton.click();
|
||||
if (event.key === "ArrowLeft") prevButton.click();
|
||||
if (event.key === "ArrowRight") nextButton.click();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,21 +1,18 @@
|
||||
---
|
||||
import "flowbite";
|
||||
import { createMarkdownComponent } from "@/base/index.js";
|
||||
import { translate } from "@polymech/astro-base/base/i18n.js";
|
||||
import Translate from "@polymech/astro-base/components/i18n.astro";
|
||||
import LGallery from "@polymech/astro-base/components/GalleryK.astro";
|
||||
import { createMarkdownComponent } from "../base/index.js";
|
||||
import { translate } from "../base/i18n.js";
|
||||
import Translate from "../components/i18n.astro";
|
||||
import LGallery from "../components/GalleryK.astro";
|
||||
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import Wrapper from "@/components/containers/Wrapper.astro";
|
||||
|
||||
import Readme from "@/components/polymech/readme.astro";
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro";
|
||||
|
||||
import Resources from "@/components/polymech/resources.astro";
|
||||
import Specs from "@polymech/astro-base/components/specs.astro";
|
||||
|
||||
import TabButton from "@polymech/astro-base/components/tab-button.astro";
|
||||
import TabContent from "@polymech/astro-base/components/tab-content.astro";
|
||||
import Wrapper from "../components/containers/Wrapper.astro";
|
||||
import Readme from "../components/polymech/readme.astro";
|
||||
import Breadcrumb from "../components/Breadcrumb.astro";
|
||||
import Resources from "../components/polymech/resources.astro";
|
||||
import Specs from "../components/specs.astro";
|
||||
import TabButton from "../components/tab-button.astro";
|
||||
import TabContent from "../components/tab-content.astro";
|
||||
|
||||
import {
|
||||
I18N_SOURCE_LANGUAGE,
|
||||
@ -60,7 +57,7 @@ const Content_Debug = await createMarkdownComponent(str_debug);
|
||||
|
||||
<BaseLayout frontmatter={data} description={data.description} {...rest}>
|
||||
<Wrapper>
|
||||
<Breadcrumb
|
||||
<Breadcrumb
|
||||
currentPath={Astro.url.pathname}
|
||||
collection="store"
|
||||
title={data.title}
|
||||
@ -71,19 +68,19 @@ const Content_Debug = await createMarkdownComponent(str_debug);
|
||||
<!-- Left Column: Description -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 class=" font-semibold mb-2 text-2xl">
|
||||
<h1 class="font-semibold mb-2 text-2xl">
|
||||
<Translate>{`${data.title}`}</Translate>
|
||||
</h1>
|
||||
{
|
||||
isRTL(Astro.currentLocale) && (
|
||||
<div class=" font-semibold mb-2">
|
||||
"{data.title}"
|
||||
</div>
|
||||
<div class=" font-semibold mb-2">"{data.title}"</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<article class="markdown-content bg-white dark:bg-gray-800 rounded-xl p-4">
|
||||
|
||||
<article
|
||||
class="markdown-content bg-white dark:bg-gray-800 rounded-xl p-4"
|
||||
>
|
||||
<Body />
|
||||
</article>
|
||||
|
||||
@ -146,12 +143,12 @@ const Content_Debug = await createMarkdownComponent(str_debug);
|
||||
|
||||
{
|
||||
SHOW_CHECKOUT && data.checkout && (
|
||||
<a
|
||||
href={data.checkout}
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white dark:bg-gray-800 hover:bg-black dark:hover:bg-gray-700 duration-300 rounded-xl w-full justify-between"
|
||||
>
|
||||
<a
|
||||
href={data.checkout}
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white dark:bg-gray-800 hover:bg-black dark:hover:bg-gray-700 duration-300 rounded-xl w-full justify-between"
|
||||
>
|
||||
<span class="relative uppercase text-xs ">
|
||||
<Translate>Add to cart</Translate>
|
||||
</span>
|
||||
@ -202,7 +199,9 @@ const Content_Debug = await createMarkdownComponent(str_debug);
|
||||
<h3 class="text-lg text-neutral-600 dark:text-gray-300 uppercase tracking-tight">
|
||||
License
|
||||
</h3>
|
||||
<p class=" mt-4 text-sm text-gray-700 dark:text-gray-300">{data.license}</p>
|
||||
<p class=" mt-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{data.license}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -220,149 +219,147 @@ const Content_Debug = await createMarkdownComponent(str_debug);
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{data.assets.showcase && data.assets.showcase.length > 0 && (
|
||||
<section>
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-4 md:mb-16 mt-0 p-2 md:p-4 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<LGallery
|
||||
images={data.assets.showcase}
|
||||
lightboxSettings={{
|
||||
SHOW_TITLE: false,
|
||||
SHOW_DESCRIPTION: false,
|
||||
SIZES_THUMB: "w-32 h-32",
|
||||
}}
|
||||
gallerySettings={{
|
||||
SHOW_TITLE: false,
|
||||
SHOW_DESCRIPTION: false,
|
||||
//SIZES_THUMB: "w-32 h-32",
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
{
|
||||
data.assets.showcase && data.assets.showcase.length > 0 && (
|
||||
<section>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 md:mb-16 mt-0 p-2 md:p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<LGallery
|
||||
images={data.assets.showcase}
|
||||
lightboxSettings={{
|
||||
SHOW_TITLE: false,
|
||||
SHOW_DESCRIPTION: false,
|
||||
SIZES_THUMB: "w-32 h-32",
|
||||
}}
|
||||
gallerySettings={{
|
||||
SHOW_TITLE: false,
|
||||
SHOW_DESCRIPTION: false,
|
||||
//SIZES_THUMB: "w-32 h-32",
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<section id="tabs-view">
|
||||
<div class="mb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<ul
|
||||
class="flex flex-wrap -mb-px text-sm font-medium text-center"
|
||||
id="default-styled-tab"
|
||||
data-tabs-toggle="#default-styled-tab-content"
|
||||
data-tabs-active-classes="text-orange-600 hover:text-orange-600 dark:text-purple-500 dark:hover:text-purple-500 border-orange-600 dark:border-purple-500"
|
||||
data-tabs-inactive-classes="dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300"
|
||||
role="tablist"
|
||||
>
|
||||
{SHOW_README && <TabButton title="Overview" />}
|
||||
<TabButton title="Specs" />
|
||||
<TabButton title="Gallery" />
|
||||
<TabButton title="Resources" />
|
||||
{SHOW_SAMPLES && <TabButton title="Samples" />}
|
||||
{SHOW_DEBUG && <TabButton title="Debug" />}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="default-styled-tab-content">
|
||||
<TabContent title="Overview">
|
||||
<div class="mb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<ul
|
||||
class="flex flex-wrap -mb-px text-sm font-medium text-center"
|
||||
id="default-styled-tab"
|
||||
data-tabs-toggle="#default-styled-tab-content"
|
||||
data-tabs-active-classes="text-orange-600 hover:text-orange-600 dark:text-purple-500 dark:hover:text-purple-500 border-orange-600 dark:border-purple-500"
|
||||
data-tabs-inactive-classes="dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300"
|
||||
role="tablist"
|
||||
>
|
||||
{SHOW_README && <TabButton title="Overview" />}
|
||||
<TabButton title="Specs" />
|
||||
<TabButton title="Gallery" />
|
||||
<TabButton title="Resources" />
|
||||
{SHOW_SAMPLES && <TabButton title="Samples" />}
|
||||
{SHOW_DEBUG && <TabButton title="Debug" />}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="default-styled-tab-content">
|
||||
<TabContent title="Overview">
|
||||
{
|
||||
SHOW_README && data.readme && (
|
||||
<Readme markdown={data.readme} data={data} />
|
||||
)
|
||||
}
|
||||
</TabContent>
|
||||
<div
|
||||
class="hidden bg-white rounded-xl dark:bg-gray-800"
|
||||
id="specs-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<Specs frontmatter={data} />
|
||||
</div>
|
||||
<div
|
||||
class="hidden p-0 md:p-4 rounded-lg bg-white dark:bg-gray-800"
|
||||
id="gallery-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<LGallery images={data.assets.gallery} />
|
||||
</div>
|
||||
{
|
||||
SHOW_README && data.readme && (
|
||||
<Readme markdown={data.readme} data={data} />
|
||||
SHOW_SAMPLES && (
|
||||
<div
|
||||
class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
|
||||
id="samples-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<LGallery images={data.assets.samples} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</TabContent>
|
||||
<div
|
||||
class="hidden bg-white rounded-xl dark:bg-gray-800 "
|
||||
id="specs-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<Specs frontmatter={data} />
|
||||
</div>
|
||||
<div
|
||||
class="hidden p-0 md:p-4 rounded-lg bg-white dark:bg-gray-800"
|
||||
id="gallery-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<LGallery images={data.assets.gallery} />
|
||||
</div>
|
||||
{
|
||||
SHOW_SAMPLES && (
|
||||
<div
|
||||
class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
|
||||
id="samples-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<LGallery images={data.assets.samples} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
SHOW_RESOURCES && (
|
||||
<div
|
||||
class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
|
||||
id="resources-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<Resources frontmatter={data} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
SHOW_DEBUG && (
|
||||
<div
|
||||
class="hidden rounded-lg bg-white p-4 dark:bg-gray-800"
|
||||
id="debug-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<Content_Debug />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener("hashchange", () => {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
const tabTrigger = document.querySelector(
|
||||
`a[href="#${hash}"], a[data-tabs-target="#${hash}"]`,
|
||||
);
|
||||
if (tabTrigger) {
|
||||
setTimeout(() => {
|
||||
(tabTrigger as HTMLElement).click();
|
||||
}, 100);
|
||||
}
|
||||
{
|
||||
SHOW_RESOURCES && (
|
||||
<div
|
||||
class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
|
||||
id="resources-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<Resources frontmatter={data} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const tabTrigger = document.querySelector(
|
||||
`a[href="${hash}"], a[data-tabs-target="${hash}"]`,
|
||||
);
|
||||
if (tabTrigger) {
|
||||
setTimeout(() => {
|
||||
(tabTrigger as HTMLElement).click();
|
||||
}, 100);
|
||||
}
|
||||
{
|
||||
SHOW_DEBUG && (
|
||||
<div
|
||||
class="hidden rounded-lg bg-white p-4 dark:bg-gray-800"
|
||||
id="debug-view"
|
||||
role="tabpanel"
|
||||
aria-labelledby="dashboard-tab"
|
||||
>
|
||||
<Content_Debug />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
document.querySelectorAll("a[data-tabs-target]").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const href = tab.getAttribute("href");
|
||||
if (href) {
|
||||
window.location.hash = href;
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener("hashchange", () => {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
const tabTrigger = document.querySelector(
|
||||
`a[href="#${hash}"], a[data-tabs-target="#${hash}"]`,
|
||||
);
|
||||
if (tabTrigger) {
|
||||
setTimeout(() => {
|
||||
(tabTrigger as HTMLElement).click();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const tabTrigger = document.querySelector(
|
||||
`a[href="${hash}"], a[data-tabs-target="${hash}"]`,
|
||||
);
|
||||
if (tabTrigger) {
|
||||
setTimeout(() => {
|
||||
(tabTrigger as HTMLElement).click();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
document.querySelectorAll("a[data-tabs-target]").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const href = tab.getAttribute("href");
|
||||
if (href) {
|
||||
window.location.hash = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
</section>
|
||||
</Wrapper>
|
||||
</BaseLayout>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Sidebar from '@/components/sidebar/Sidebar.astro';
|
||||
import MobileToggle from '@/components/sidebar/MobileToggle.astro';
|
||||
import { getSidebarConfig } from '@/components/sidebar/config';
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import Sidebar from "../components/sidebar/Sidebar.astro";
|
||||
import MobileToggle from "../components/sidebar/MobileToggle.astro";
|
||||
import { getSidebarConfig } from "../components/sidebar/config";
|
||||
|
||||
interface Props {
|
||||
frontmatter: {
|
||||
@ -20,12 +20,12 @@ const sidebarConfig = getSidebarConfig();
|
||||
<div class="layout-with-sidebar">
|
||||
<!-- Mobile Toggle -->
|
||||
<MobileToggle />
|
||||
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar-wrapper">
|
||||
<Sidebar config={sidebarConfig} currentUrl={Astro.url} />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content-with-sidebar">
|
||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
|
||||
@ -1,49 +1,49 @@
|
||||
import type { DataEntry } from "astro:content"
|
||||
|
||||
import * as path from 'path'
|
||||
import { findUp } from 'find-up'
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
|
||||
import { env } from '@/base/index.js'
|
||||
import { env } from '../base/index.js'
|
||||
import { gallery } from '@polymech/astro-base/base/media.js';
|
||||
|
||||
import { get } from '@polymech/commons/component'
|
||||
import { PFilterValid } from '@polymech/commons/filter'
|
||||
|
||||
import { IAssemblyData } from '@polymech/cad'
|
||||
import { logger as log } from '@/base/index.js'
|
||||
import { logger as log } from '../base/index.js'
|
||||
import { translate } from "../base/i18n.js"
|
||||
import { slugify } from '../base/strings.js'
|
||||
import { loadConfig } from '../app/config-loader.js'
|
||||
|
||||
import { filesEx, forward_slash, resolveConfig, template } from '@polymech/commons'
|
||||
|
||||
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
|
||||
|
||||
import { DataEntry } from "astro:content"
|
||||
import type { Loader, LoaderContext } from 'astro/loaders'
|
||||
|
||||
import { PolymechInstance } from '../registry.js';
|
||||
|
||||
// Access config safely
|
||||
const c = () => {
|
||||
const cfg = PolymechInstance.getConfig();
|
||||
if (!cfg) throw new Error("Polymech configuration missing. Ensure setConfig is called.");
|
||||
return cfg;
|
||||
}
|
||||
import { AppConfig } from "../app/config.schema.js"
|
||||
const config = (): AppConfig => PolymechInstance.getConfig();
|
||||
|
||||
// Config Accessors
|
||||
const PRODUCT_ROOT = () => c().products?.root || '';
|
||||
const PRODUCT_GLOB = () => c().products?.glob || '';
|
||||
const PRODUCT_ROOT = () => config().products?.root || '';
|
||||
const PRODUCT_GLOB = () => config().products?.glob || '';
|
||||
const PRODUCT_DIR = (rel: string) => path.join(PRODUCT_ROOT(), rel);
|
||||
|
||||
const CAD_MAIN_MATCH = (product: string) => template(c().cad?.main_match || '', { product });
|
||||
const CAD_MAIN_MATCH = (product: string) => template(config().cad?.main_match || '', { product });
|
||||
const CAD_URL = (file: string, variables: Record<string, string>) =>
|
||||
template(c().assets?.cad_url || '', { file, ...variables });
|
||||
template(config().assets?.cad_url || '', { file, ...variables });
|
||||
|
||||
const CAD_EXTENSIONS = () => c().cad?.extensions || [];
|
||||
const CAD_EXTENSIONS = () => config().cad?.extensions || [];
|
||||
const CAD_MODEL_EXT = ".tree.json";
|
||||
const CAD_EXPORT_CONFIGURATIONS = () => c().cad?.export_configurations;
|
||||
const CAD_DEFAULT_CONFIGURATION = () => c().cad?.default_configuration || '';
|
||||
const CAD_EXPORT_CONFIGURATIONS = () => config().cad?.export_configurations;
|
||||
const CAD_DEFAULT_CONFIGURATION = () => config().cad?.default_configuration || '';
|
||||
|
||||
// Product Branches
|
||||
const PRODUCT_BRANCHES = () => {
|
||||
const enabled = c().products?.enabled;
|
||||
const enabled = config().products?.enabled;
|
||||
const resolvedPath = enabled ? path.resolve(enabled) : null;
|
||||
return (resolvedPath && exists(resolvedPath)) ? read(resolvedPath, 'json') : null;
|
||||
}
|
||||
@ -272,3 +272,32 @@ export function loader(branch: string): Loader {
|
||||
load
|
||||
};
|
||||
}
|
||||
|
||||
export const group_path = (item) => item.id.split("/")[1]
|
||||
|
||||
const group_label = async (text: string, locale) => await translate(slugify(text), config().i18n.source_language, locale)
|
||||
|
||||
const group = async (items, locale) => {
|
||||
return items.reduce(async (accPromise, item) => {
|
||||
const acc = await accPromise
|
||||
const id = group_path(item)
|
||||
let key: string = await group_label(id, locale)
|
||||
key = key.charAt(0).toUpperCase() + key.slice(1)
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(item)
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const group_by_path = async (items, locale): Promise<IComponentConfigEx[]> => await group(items, locale)
|
||||
|
||||
export const mailto = (to: string, subject: string, body: string): string => {
|
||||
const encode = (str: string) => encodeURIComponent(str).replace(/%20/g, '+');
|
||||
return `mailto:${encode(to)}?subject=${encode(subject)}&body=${encode(body)}`;
|
||||
}
|
||||
|
||||
export const item_checkout = async (item: IComponentConfig) => {
|
||||
// return `mailto:${DEFAULT_CONTACT}?subject=${item.name}&body=${""}`
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import type { IComponentNode, IComponentConfig } from '@polymech/commons'
|
||||
import config from "../app/config.json"
|
||||
import { PolymechInstance } from '../registry.js';
|
||||
const config = () => PolymechInstance.getConfig();
|
||||
|
||||
interface ProductJsonLD {
|
||||
'@context': 'https://schema.org'
|
||||
@ -21,18 +22,18 @@ interface ProductJsonLD {
|
||||
}
|
||||
}
|
||||
|
||||
export const get = async (node: IComponentNode, component: IComponentConfig, opts:{
|
||||
url?:string
|
||||
export const get = async (node: IComponentNode, component: IComponentConfig, opts: {
|
||||
url?: string
|
||||
}): Promise<ProductJsonLD> => {
|
||||
const jsonLD: ProductJsonLD = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
'@type': 'Product',
|
||||
name: component.name,
|
||||
description: component.keywords,
|
||||
sku: component.code,
|
||||
brand: {
|
||||
'@type': 'Brand',
|
||||
name: config.ecommerce?.brand || config.site.title
|
||||
name: config().ecommerce?.brand || config().site.title
|
||||
}
|
||||
}
|
||||
if (component.image?.url) {
|
||||
@ -43,9 +44,9 @@ export const get = async (node: IComponentNode, component: IComponentConfig, opt
|
||||
jsonLD.offers = {
|
||||
'@type': 'Offer',
|
||||
price: component.price,
|
||||
priceCurrency: config.ecommerce?.currencyCode || 'EU',
|
||||
priceCurrency: config().ecommerce?.currencyCode || 'EU',
|
||||
availability: 'https://schema.org/InStock',
|
||||
url: opts.url || config.site.base_url
|
||||
url: opts.url || config().site.base_url
|
||||
}
|
||||
}
|
||||
return jsonLD
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { IComponentNode, IComponentConfig } from '@polymech/commons'
|
||||
|
||||
import config from "../app/config.json"
|
||||
|
||||
interface GoogleMerchantProduct {
|
||||
id: string
|
||||
title: string
|
||||
@ -16,7 +14,7 @@ interface GoogleMerchantProduct {
|
||||
}
|
||||
|
||||
export const get = async (node: IComponentNode, config: IComponentConfig, opts: {
|
||||
url?:string
|
||||
url?: string
|
||||
}): Promise<GoogleMerchantProduct> => {
|
||||
const product: GoogleMerchantProduct = {
|
||||
id: config.code,
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
---
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
Astro.redirect("/en/home");
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<section>
|
||||
<div
|
||||
class="flex flex-col gap-12 h-full justify-between p-4 text-center py-20">
|
||||
class="flex flex-col gap-12 h-full justify-between p-4 text-center py-20"
|
||||
>
|
||||
<div class="max-w-xl mx-auto">
|
||||
<h1
|
||||
class="text-lg text-neutral-600 tracking-tight text-balance">
|
||||
<h1 class="text-lg text-neutral-600 tracking-tight text-balance">
|
||||
404 Page not found
|
||||
</h1>
|
||||
<p class="text-sm text-balance ">
|
||||
<p class="text-sm text-balance">
|
||||
The page you are looking for does not exist. Please try again. If the
|
||||
problem persists, please contact us.
|
||||
</p>
|
||||
@ -21,13 +21,15 @@ Astro.redirect("/en/home");
|
||||
href="/"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white hover:bg-neutral-200 duration-300 rounded-xl w-full justify-between">
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white hover:bg-neutral-200 duration-300 rounded-xl w-full justify-between"
|
||||
>
|
||||
<span class="relative uppercase text-xs text-orange-600"
|
||||
>Go home</span
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="w-12 text-orange-600 transition duration-300 -translate-y-7 group-hover:translate-y-7">
|
||||
class="w-12 text-orange-600 transition duration-300 -translate-y-7 group-hover:translate-y-7"
|
||||
>
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -35,12 +37,12 @@ Astro.redirect("/en/home");
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
class="size-6 m-auto fill-white"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-14 flex">
|
||||
@ -50,12 +52,12 @@ Astro.redirect("/en/home");
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
class="size-6 m-auto fill-white"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -64,13 +66,13 @@ Astro.redirect("/en/home");
|
||||
href="/forms/contact"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center hover:bg-black duration-300 rounded-xl w-full justify-between">
|
||||
<span class="relative uppercase text-xs "
|
||||
>Contact us</span
|
||||
>
|
||||
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center hover:bg-black duration-300 rounded-xl w-full justify-between"
|
||||
>
|
||||
<span class="relative uppercase text-xs">Contact us</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7">
|
||||
class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7"
|
||||
>
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -78,12 +80,12 @@ Astro.redirect("/en/home");
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
class="size-6 m-auto fill-white"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-14 flex">
|
||||
@ -93,12 +95,12 @@ Astro.redirect("/en/home");
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
class="size-6 m-auto fill-white"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,28 +1,29 @@
|
||||
---
|
||||
import { getCollection, render } from "astro:content"
|
||||
import Resources from "@/layouts/Resources.astro"
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro"
|
||||
import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro"
|
||||
import { getSidebarConfig } from '@polymech/astro-base/config/sidebar';
|
||||
import ResourceCard from "@polymech/astro-base/components/resources/ResourceCard.astro";
|
||||
import Resources from "../../../layouts/Resources.astro"
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import Sidebar from "../../../components/sidebar/Sidebar.astro"
|
||||
import MobileToggle from "../../../components/sidebar/MobileToggle.astro"
|
||||
import { getSidebarConfig } from '../../../components/sidebar/config';
|
||||
import ResourceCard from "../../../components/resources/ResourceCard.astro";
|
||||
|
||||
import { generateBreadcrumbs, calculateReadingTime, getStaticPaths_fs } from '@polymech/astro-base/base/collections';
|
||||
import { generateBreadcrumbs, calculateReadingTime, getStaticPaths_fs } from '../../../base/collections';
|
||||
|
||||
import { translate } from "@polymech/astro-base/base/i18n.js";
|
||||
import Translate from "@polymech/astro-base/components/i18n.astro";
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import {translate } from "../../../base/i18n.js";
|
||||
import Translate from "../../../components/i18n.astro";
|
||||
import { I18N_SOURCE_LANGUAGE } from "../../../app/config.js";
|
||||
|
||||
const collectionName = 'resources';
|
||||
const collectionDescription = 'Discover insights, tutorials, and best practices from our collection of technical articles and resources.';
|
||||
import { PolymechInstance } from '@polymech/astro-base/registry';
|
||||
import { PolymechInstance } from '../../../registry';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const config = PolymechInstance.getConfig();
|
||||
const collectionName = 'resources';
|
||||
const paths = await getStaticPaths_fs(getCollection, collectionName, config.LANGUAGES_PROD, config.COLLECTION_FILTERS);
|
||||
const paths = await getStaticPaths_fs(getCollection, collectionName, config.core.languages_prod, { });
|
||||
|
||||
// Add root path for each language
|
||||
config.LANGUAGES_PROD?.forEach((lang) => {
|
||||
config.core.languages_prod?.forEach((lang) => {
|
||||
paths.push({
|
||||
params: {
|
||||
locale: lang
|
||||
|
||||
@ -1,30 +1,31 @@
|
||||
---
|
||||
import { getCollection, render } from "astro:content"
|
||||
import Resources from "@/layouts/Resources.astro"
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro"
|
||||
import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro"
|
||||
import { getSidebarConfig } from '@polymech/astro-base/config/sidebar';
|
||||
import ResourceCard from "@/components/resources/ResourceCard.astro";
|
||||
import { LANGUAGES_PROD, COLLECTION_FILTERS } from "config/config.js"
|
||||
import Resources from "../../../layouts/Resources.astro"
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import Sidebar from "../../../components/sidebar/Sidebar.astro"
|
||||
import MobileToggle from "../../../components/sidebar/MobileToggle.astro"
|
||||
|
||||
import { generateBreadcrumbs, calculateReadingTime, getStaticPaths_fs } from '@polymech/astro-base/base/collections';
|
||||
import { getSidebarConfig } from '../../../config/sidebar';
|
||||
|
||||
import ResourceCard from "../../../components/resources/ResourceCard.astro";
|
||||
|
||||
|
||||
import { generateBreadcrumbs, calculateReadingTime, getStaticPaths_fs } from '../../../base/collections';
|
||||
|
||||
import { translate } from "@polymech/astro-base/base/i18n.js";
|
||||
import Translate from "@polymech/astro-base/components/i18n.astro";
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
|
||||
import { I18N_SOURCE_LANGUAGE } from "../../../app/config.js";
|
||||
|
||||
const collectionName = 'resources';
|
||||
const collectionDescription = 'Discover insights, tutorials, and best practices from our collection of technical articles and resources.';
|
||||
import { PolymechInstance } from '@polymech/astro-base/registry';
|
||||
export async function getStaticPaths() {
|
||||
debugger;
|
||||
const config = PolymechInstance.getConfig();
|
||||
const collectionName = 'resources';
|
||||
const paths = await getStaticPaths_fs(getCollection, collectionName, LANGUAGES_PROD, COLLECTION_FILTERS);
|
||||
|
||||
const paths = await getStaticPaths_fs(getCollection, collectionName, config.core.languages_prod, {});
|
||||
// Add root path for each language
|
||||
LANGUAGES_PROD.forEach((lang) => {
|
||||
config.core.languages_prod.forEach((lang) => {
|
||||
paths.push({
|
||||
params: {
|
||||
locale: lang,
|
||||
|
||||
@ -1,84 +1,95 @@
|
||||
---
|
||||
import Resources from "@/layouts/Resources.astro";
|
||||
import ResourceCard from "@/components/resources/ResourceCard.astro";
|
||||
import Resources from "../../../../layouts/Resources.astro";
|
||||
import ResourceCard from "../../../../components/resources/ResourceCard.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import { LANGUAGES_PROD } from "config/config.js";
|
||||
import Translate from "@polymech/astro-base/components/i18n.astro";
|
||||
import { LANGUAGES_PROD } from "../../../../app/config.js";
|
||||
import Translate from "../../../../components/i18n.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await getCollection("resources");
|
||||
const uniqueTags = [
|
||||
...new Set(allPosts.map((post) => post.data.tags).flat()),
|
||||
];
|
||||
|
||||
|
||||
const paths: any[] = [];
|
||||
|
||||
|
||||
// Generate paths for each locale and tag combination
|
||||
LANGUAGES_PROD.forEach((locale) => {
|
||||
uniqueTags.forEach((tag) => {
|
||||
const filteredPosts = allPosts.filter((post) =>
|
||||
post.data.tags.includes(tag)
|
||||
post.data.tags.includes(tag),
|
||||
);
|
||||
|
||||
|
||||
paths.push({
|
||||
params: {
|
||||
params: {
|
||||
locale,
|
||||
tag
|
||||
tag,
|
||||
},
|
||||
props: {
|
||||
props: {
|
||||
posts: filteredPosts,
|
||||
locale,
|
||||
tag
|
||||
tag,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
const { tag, locale } = Astro.params;
|
||||
const { posts } = Astro.props;
|
||||
|
||||
const collectionName = posts.length > 0 ? posts[0].collection : 'resources';
|
||||
const collectionName = posts.length > 0 ? posts[0].collection : "resources";
|
||||
---
|
||||
|
||||
<Resources
|
||||
<Resources
|
||||
frontmatter={{
|
||||
title: `${collectionName.charAt(0).toUpperCase() + collectionName.slice(1)} tagged with ${tag}`,
|
||||
description: `${posts.length} ${collectionName}${posts.length !== 1 ? 's' : ''} found`,
|
||||
description: `${posts.length} ${collectionName}${posts.length !== 1 ? "s" : ""} found`,
|
||||
breadcrumb: true,
|
||||
bottomNav: 'TOP'
|
||||
bottomNav: "TOP",
|
||||
}}
|
||||
collectionName={collectionName}
|
||||
>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-6 py-4">
|
||||
{posts.map((post) => (
|
||||
<ResourceCard
|
||||
url={`/${locale}/${collectionName}/${post.id}`}
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
alt={post.data.image?.alt || post.data.title}
|
||||
pubDate={post.data.pubDate.toLocaleDateString()}
|
||||
author={post.data.author}
|
||||
image={post.data.image?.url}
|
||||
tags={post.data.tags}
|
||||
path={post.id}
|
||||
locale={locale}
|
||||
contentId={post.id}
|
||||
collectionName={collectionName}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
posts.map((post) => (
|
||||
<ResourceCard
|
||||
url={`/${locale}/${collectionName}/${post.id}`}
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
alt={post.data.image?.alt || post.data.title}
|
||||
pubDate={post.data.pubDate.toLocaleDateString()}
|
||||
author={post.data.author}
|
||||
image={post.data.image?.url}
|
||||
tags={post.data.tags}
|
||||
path={post.id}
|
||||
locale={locale}
|
||||
contentId={post.id}
|
||||
collectionName={collectionName}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Back to Tags -->
|
||||
<div class="mt-12 text-center">
|
||||
<a
|
||||
<a
|
||||
href={`/${locale}/${collectionName}/tags/`}
|
||||
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
<Translate>Back to All Tags</Translate>
|
||||
</a>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
---
|
||||
import StoreLayout from "@/layouts/StoreLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import { LANGUAGES_PROD } from "config/config.js";
|
||||
import StoreEntries from "@/components/store/StoreEntries.astro";
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import Wrapper from "@/components/containers/Wrapper.astro";
|
||||
import Translate from "@polymech/astro-base/components/i18n.astro";
|
||||
import { slugify } from "@/base/strings.js";
|
||||
import { LANGUAGES_PROD } from "../../../app/config.js";
|
||||
|
||||
import StoreEntries from "../../../components/store/StoreEntries.astro";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import Wrapper from "../../../components/containers/Wrapper.astro";
|
||||
import Translate from "../../../components/i18n.astro";
|
||||
import { slugify } from "../../../base/strings.js";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const view = "store";
|
||||
|
||||
@ -1,25 +1,28 @@
|
||||
---
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro"
|
||||
import { getCollection } from "astro:content"
|
||||
import StoreEntries from "@/components/store/StoreEntries.astro"
|
||||
const allProducts = await getCollection("store")
|
||||
const locale = Astro.currentLocale || "en"
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
import StoreEntries from "../components/store/StoreEntries.astro";
|
||||
|
||||
const allProducts = await getCollection("store");
|
||||
const locale = Astro.currentLocale || "en";
|
||||
---
|
||||
<BaseLayout>
|
||||
|
||||
<BaseLayout>
|
||||
<section>
|
||||
<div class="py-2 space-y-2">
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-2 gap-2 ">
|
||||
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-2 gap-2"></div>
|
||||
<a
|
||||
href="/blog/home"
|
||||
title="link to your page"
|
||||
aria-label="your label"
|
||||
class="relative group overflow-hidden pl-4 justify-between text-xs text-orange-600 h-14 flex space-x-6 items-center bg-white hover:bg-neutral-200 hover:text-orange-600 duration-300 rounded-xl">
|
||||
class="relative group overflow-hidden pl-4 justify-between text-xs text-orange-600 h-14 flex space-x-6 items-center bg-white hover:bg-neutral-200 hover:text-orange-600 duration-300 rounded-xl"
|
||||
>
|
||||
<span class="relative uppercase text-xs">Read all articles</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7">
|
||||
class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7"
|
||||
>
|
||||
<div class="h-14 flex">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -27,12 +30,12 @@ const locale = Astro.currentLocale || "en"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
class="size-6 m-auto fill-white"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="h-14 flex">
|
||||
@ -42,12 +45,12 @@ const locale = Astro.currentLocale || "en"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6 m-auto fill-white">
|
||||
class="size-6 m-auto fill-white"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
|
||||
></path>
|
||||
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,11 +59,10 @@ const locale = Astro.currentLocale || "en"
|
||||
</section>
|
||||
<section>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-2">
|
||||
|
||||
{
|
||||
allProducts.map((post) => (
|
||||
<StoreEntries
|
||||
url={ locale + "/store/" + post.id}
|
||||
url={locale + "/store/" + post.id}
|
||||
title={post.data.title}
|
||||
price={post.data.price}
|
||||
type={post.data.type}
|
||||
@ -71,6 +73,4 @@ const locale = Astro.currentLocale || "en"
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { AppConfig } from './app/config.schema.js'
|
||||
interface PolymechConfig {
|
||||
// Config groups matching app-config.json
|
||||
site?: any;
|
||||
@ -87,7 +88,7 @@ if (!G.__MYPAGES_CONFIG__) {
|
||||
|
||||
class PolymechRegistry {
|
||||
private static instance: PolymechRegistry;
|
||||
private config: PolymechConfig;
|
||||
private config: AppConfig;
|
||||
private callbacks: Record<string, Function[]> = {};
|
||||
|
||||
constructor() {
|
||||
@ -103,19 +104,19 @@ class PolymechRegistry {
|
||||
}
|
||||
|
||||
// Set configuration from host app
|
||||
setConfig(config: PolymechConfig) {
|
||||
setConfig(config: AppConfig) {
|
||||
// Merge the new config into the existing one and update the global store.
|
||||
const newConfig = { ...this.config, ...config };
|
||||
this.config = newConfig;
|
||||
G.__MYPAGES_CONFIG__ = newConfig;
|
||||
|
||||
// Trigger callbacks
|
||||
this.config.callbacks?.onConfigUpdate?.(this.config);
|
||||
(this.config as any).callbacks?.onConfigUpdate?.(this.config);
|
||||
this.emit('configUpdate', { newConfig: this.config });
|
||||
}
|
||||
|
||||
// Get configuration in components
|
||||
getConfig(): PolymechConfig {
|
||||
getConfig(): AppConfig {
|
||||
// The config property is now always synced with the global store.
|
||||
return this.config;
|
||||
}
|
||||
@ -136,9 +137,10 @@ class PolymechRegistry {
|
||||
|
||||
// Convenience getters
|
||||
// Convenience getters (Mapping new config structure to old accessors if needed, or just helpers)
|
||||
get productCategory() { return this.config.productCategory || 'Unknown'; }
|
||||
|
||||
get languages() { return this.config.core?.languages || ['en']; }
|
||||
get LANGUAGES_PROD() { return this.config.core?.languages_prod || ['en']; }
|
||||
get COLLECTION_FILTERS() { return {}; }
|
||||
// products was string[]? in old config it was string[], now it's an object config.products
|
||||
// Old: get products() { return this.config.products || []; }
|
||||
// We can leave 'products' as the category list if it exists in 'site' or somewhere?
|
||||
@ -146,8 +148,6 @@ class PolymechRegistry {
|
||||
// If the old getter meant 'list of product categories', that's not in the new config yet?
|
||||
// Let's assume accessing raw config is preferred now.
|
||||
|
||||
get apiEndpoints() { return this.config.apiEndpoints || {}; }
|
||||
get COLLECTION_FILTERS() { return this.config.COLLECTION_FILTERS || {}; }
|
||||
}
|
||||
|
||||
export const PolymechInstance = PolymechRegistry.getInstance();
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { resolveVariables, resolve } from '@polymech/commons'
|
||||
|
||||
// --- Debug Configuration ---
|
||||
const enableDebugSuccess = false;
|
||||
const enableDebugErrors = false;
|
||||
const enableDebugErrors = false;
|
||||
|
||||
/**
|
||||
* Checks if a given file path exists and is a file.
|
||||
@ -72,7 +72,7 @@ export function resolveImagePath(src: string, entryPath?: string, astroUrl?: URL
|
||||
const pathSegments = astroUrl.pathname.split('/').filter(p => p);
|
||||
const hasLocale = pathSegments.length > 0 && /^[a-z]{2}$/.test(pathSegments[0]);
|
||||
if (hasLocale) pathSegments.shift();
|
||||
|
||||
|
||||
if (pathSegments.length >= 1) {
|
||||
const contentDirGuess = isFolderUrl ? pathSegments.join('/') : pathSegments.slice(0, -1).join('/');
|
||||
basePath = path.join(process.cwd(), 'src', 'content', contentDirGuess);
|
||||
@ -88,7 +88,7 @@ export function resolveImagePath(src: string, entryPath?: string, astroUrl?: URL
|
||||
}
|
||||
|
||||
// Parent Directory Check: If not found, check one level
|
||||
|
||||
|
||||
// if (enableDebugErrors) console.warn(`[resolveImagePath] [WARN-${strategy}] Not found in "${basePath}". Checking parent directory.`);
|
||||
const parentBasePath = path.resolve(basePath, '..');
|
||||
resolvedPath = checkFilePath(parentBasePath, src);
|
||||
@ -101,7 +101,7 @@ export function resolveImagePath(src: string, entryPath?: string, astroUrl?: URL
|
||||
console.warn(`[resolveImagePath] [WARN-${strategy}] Final path check failed for "${src}". Base path checked: "${basePath}", Parent path checked: "${parentBasePath}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (enableDebugErrors) {
|
||||
console.error(`[resolveImagePath] [FAILURE] Could not resolve relative path "${src}". Returning original.`);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"isolatedModules": true,
|
||||
// Astro will directly run your TypeScript code, no transpilation needed.
|
||||
"outDir": "./dist",
|
||||
"rootDir": "src",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
@ -43,9 +44,14 @@
|
||||
},
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.astro"
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.astro"
|
||||
],
|
||||
"exclude": [
|
||||
"src/components/sidebar/**/*",
|
||||
"src/config/astro-config.ts",
|
||||
"src/components/polymech/renderer.ts"
|
||||
],
|
||||
"files": [
|
||||
"src/index.ts"
|
||||
|
||||
11
packages/polymech/vitest.config.ts
Normal file
11
packages/polymech/vitest.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias:
|
||||
{
|
||||
"src/*": "/src/*",
|
||||
"app/*": "/src/app/*",
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user