This commit is contained in:
lovebird 2025-03-19 23:18:13 +01:00
parent f82172797f
commit 48127f4534
33 changed files with 157 additions and 1015 deletions

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-03-17T18:23:55.481Z"
"default": "2025-03-19T21:59:23.180Z"
},
"description": {
"type": "string",

File diff suppressed because one or more lines are too long

View File

@ -43,8 +43,7 @@ export default defineConfig({
resolve: {
alias: {
'@components': '/src/components',
'@layouts': '/src/layouts',
"base": "../polymech-site/src"
'@layouts': '/src/layouts'
}
},
plugins: [

33
package-lock.json generated
View File

@ -15,8 +15,8 @@
"@astrojs/sitemap": "^3.2.1",
"@astrolib/seo": "^1.0.0-beta.8",
"@jsdevtools/rehype-toc": "^3.0.2",
"@plastichub/astro-site-template": "file:../polymech-site",
"@playwright/test": "^1.50.1",
"@polymech/astro-base": "file:../astro-components/packages/polymech",
"@polymech/cache": "file:../polymech-mono/packages/cache",
"@polymech/cad": "file:../polymech-mono/packages/cad",
"@polymech/commons": "file:../polymech-mono/packages/commons",
@ -83,6 +83,18 @@
"sass-embedded": "^1.83.4"
}
},
"../astro-components/packages/polymech": {
"name": "@polymech/astro-base",
"version": "0.5.6",
"dependencies": {
"@polymech/cad": "file:../../../polymech-mono/packages/cad",
"@polymech/fs": "file:../../../polymech-mono/packages/fs",
"@polymech/i18n": "file:../../../polymech-mono/packages/i18n",
"@polymech/kbot-d": "file:../../../polymech-mono/packages/kbot",
"@polymech/log": "file:../../../polymech-mono/packages/log",
"react-jsx-parser": "^2.4.0"
}
},
"../polymech-mono/packages/cache": {
"name": "@polymech/cache",
"version": "0.4.8",
@ -264,8 +276,9 @@
"marked": "14.1.4",
"marked-terminal": "7.2.1",
"mime-types": "2.1.35",
"openai": "4.85.3",
"openai": "4.87.4",
"p-map": "7.0.3",
"ts-retry": "6.0.0",
"tslog": "^4.9.3",
"yargs": "17.7.2",
"zod": "3.24.2"
@ -316,6 +329,7 @@
"../polymech-site": {
"name": "@plastichub/astro-site-template",
"version": "0.0.1",
"extraneous": true,
"dependencies": {
"@astrojs/compiler": "^2.10.4",
"@astrojs/mdx": "^4.1.0",
@ -325,6 +339,8 @@
"@astrolib/seo": "^1.0.0-beta.8",
"@jsdevtools/rehype-toc": "^3.0.2",
"@playwright/test": "^1.50.1",
"@polymech/astro-base": "file:../astro-components/packages/polymech",
"@polymech/astro-components": "file:../astro-components/packages/astro-components",
"@polymech/cache": "file:../polymech-mono/packages/cache",
"@polymech/cad": "file:../polymech-mono/packages/cad",
"@polymech/commons": "file:../polymech-mono/packages/commons",
@ -349,7 +365,7 @@
"github-slugger": "^2.0.0",
"glob": "^11.0.1",
"got": "^14.4.6",
"imagetools": "file:packages/imagetools",
"imagetools": "file:../astro-components/packages/imagetools",
"lighthouse": "^12.3.0",
"markdown-it": "^14.1.0",
"marked": "^15.0.7",
@ -363,7 +379,7 @@
"p-map": "^7.0.3",
"picomatch": "^4.0.2",
"potrace": "^2.1.8",
"react-jsx-parser": "^2.3.0",
"react-jsx-parser": "^2.4.0",
"reading-time": "^1.5.0",
"rehype-accessible-emojis": "^0.3.2",
"rehype-stringify": "^10.0.1",
@ -376,6 +392,7 @@
"sharp": "^0.29.3",
"showdown": "^2.1.0",
"tailwindcss": "^4.0.7",
"ts-retry": "^6.0.0",
"type-fest": "^4.34.1",
"vite": "^6.1.1",
"vite-plugin-compression": "^0.5.1",
@ -2395,10 +2412,6 @@
"node": ">=14"
}
},
"node_modules/@plastichub/astro-site-template": {
"resolved": "../polymech-site",
"link": true
},
"node_modules/@playwright/test": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz",
@ -2414,6 +2427,10 @@
"node": ">=18"
}
},
"node_modules/@polymech/astro-base": {
"resolved": "../astro-components/packages/polymech",
"link": true
},
"node_modules/@polymech/cache": {
"resolved": "../polymech-mono/packages/cache",
"link": true

View File

@ -26,6 +26,7 @@
"@astrolib/seo": "^1.0.0-beta.8",
"@jsdevtools/rehype-toc": "^3.0.2",
"@playwright/test": "^1.50.1",
"@polymech/astro-base": "file:../astro-components/packages/polymech",
"@polymech/cache": "file:../polymech-mono/packages/cache",
"@polymech/cad": "file:../polymech-mono/packages/cad",
"@polymech/commons": "file:../polymech-mono/packages/commons",

View File

@ -10,7 +10,7 @@ import {
I18N_SOURCE_LANGUAGE,
PRODUCT_SPECS,
RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS
} from '@/app/config.js'
} from 'config/config.js'
import { translateXLS } from '@polymech/i18n/translate_xls'
import { I18N_STORE, OSR_ROOT } from 'config/config.js'
@ -32,7 +32,7 @@ export const translate = async (text: string, srcLanguage = 'en', targetLanguage
})
return translation
} catch (e) {
logger.error(`Failed to translate text: ${text} from ${srcLanguage} to ${targetLanguage} : ${e.message}`, e)
logger.error(`Failed to translate text: ${text} from ${srcLanguage} to ${targetLanguage} : ${e.message}`)
}
return text
}
@ -67,6 +67,6 @@ export const translateSheets = async (product, language) => {
try {
return await translateXLS(path.resolve(src), dst, i18nOptions)
} catch (e) {
logger.error(`Failed to translate assets ${src} to ${language}`, e.message, e)
logger.error(`Failed to translate assets ${src} to ${language}`, e.message)
}
}

View File

@ -8,18 +8,14 @@ import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
import { findUp } from 'find-up'
import { createLogger } from '@polymech/log'
import { parse, IProfile } from '@polymech/commons/profile'
import { renderMarkup } from "@/model/component.js";
import {
LOGGING_NAMESPACE,
OSRL_ENV,
OSRL_PRODUCT_PROFILE,
PRODUCT_ROOT,
I18N_SOURCE_LANGUAGE
PRODUCT_ROOT
} from 'config/config.js'
import { translate } from "@/base/i18n.js"
export const logger = createLogger(LOGGING_NAMESPACE)
export const boot = () => logger.info('Astro is booting up')
export const env = (item_rel: string = ""): IProfile => {
@ -38,22 +34,12 @@ export const render = async (string) => {
const html = `${unescapeHTML(string)}`
return createComponent(() => renderTemplate(html as any, []))
}
export const component = async (str: string = "", locale, data = {}) => {
const content = await translate(
(str || "").trim(),
I18N_SOURCE_LANGUAGE,
locale
)
const markup = ((await renderMarkup(content, data)) as any) || { html: "" }
return await render(markup.html)
}
export const item_defaults = async (itemDir) => {
return await findUp('defaults.json', {
stopAt: PRODUCT_ROOT(),
cwd: itemDir
})
}
export async function markdownToHtml(markdown: string): Promise<string> {
const result = await unified()
.use(remarkParse)
@ -63,11 +49,9 @@ export async function markdownToHtml(markdown: string): Promise<string> {
return result.toString();
}
export const createMarkdownComponent = async (markdown: string) => {
const html = unescapeHTML(await markdownToHtml(markdown));
return createComponent(() => renderTemplate(html as any, []));
}
export const createHTMLComponent = async (html: string) =>
createComponent(() => renderTemplate(unescapeHTML(html) as any, []))

View File

@ -3,25 +3,32 @@ import pMap from 'p-map'
import { GlobOptions } from 'glob'
import { sanitizeUri } from 'micromark-util-sanitize-uri'
import ExifReader from 'exifreader'
import { loadImage } from "imagetools/api"
import { sanitizeFilename } from '@polymech/fs/utils'
import { resolve } from '@polymech/commons'
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 { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.js'
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL } from '../app/config.js'
import { env } from './index.js'
import { GalleryImage, MetaJSON } from './images.js'
import { env } from './index.js'
import {
removeArrayValues,
removeArrays,
removeBufferValues,
removeEmptyObjects
} from '@/base/objects.js'
const IMAGES_GLOB = '*.+(JPG|jpg|png|PNG|gif)'
import {
ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT,
DEFAULT_IMAGE_URL, ASSETS_LOCAL,
ASSETS_GLOB
} from 'config/config.js'
export const default_sort = (files: string[]): string[] =>
{
export const default_sanitizer = (files:string[]) => files.map((f) => sanitizeFilename(f))
export const default_sort = (files: string[]): string[] => {
const getSortableParts = (filename: string) => {
const baseName = path.parse(filename).name;
const match = baseName.match(/^(\d+)_?(.*)$/); // Match leading numbers
@ -29,8 +36,7 @@ export const default_sort = (files: string[]): string[] =>
const textPart = match ? match[2] : baseName; // Extract text part
return { numPart, textPart };
};
}
return files.sort((a, b) => {
const { numPart: aNum, textPart: aText } = getSortableParts(a)
const { numPart: bNum, textPart: bText } = getSortableParts(b)
@ -38,10 +44,29 @@ export const default_sort = (files: string[]): string[] =>
return aNum - bNum || aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' })
}
return aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' })
});
})
}
export const default_filter = async (url: string) => {
try {
const response = await fetch(url, { method: 'HEAD' })
if (!response.ok) {
logger.warn(`Image URL not found ${url}`)
return false
}
} catch (error) {
logger.warn(`Image URL not found ${url} : ${error.message}`)
return false
}
return true
}
export const default_filter_locale = async (url: string) => {
return url && exists(url)
}
export const image_url = async (src, fallback = DEFAULT_IMAGE_URL) => {
if (exists(src)) return src
let safeSrc = src
try {
const response = await fetch(src, { method: 'HEAD' })
@ -55,25 +80,23 @@ export const image_url = async (src, fallback = DEFAULT_IMAGE_URL) => {
return safeSrc
}
export const gallery = async (
assetPath,
product): Promise<GalleryImage[] | undefined> => {
product = '' + product
export const gallery = async ( assetPath, item): Promise<GalleryImage[]> => {
const root = resolve(PRODUCT_ROOT())
const profile = env()
const assetSlug = path.parse(assetPath).name
const productConfig: any = read(PRODUCT_CONFIG(product), "json")
if (!productConfig) {
logger.warn(`item gallery : item ${product} config not found !`)
return
}
const mediaPath = `${root}/${product}/${assetPath}/`
if (!exists(mediaPath)) {
logger.warn(`item gallery : item ${product} media path not found ${mediaPath}!`)
const itemConfig: any = read(PRODUCT_CONFIG(item), "json")
if (!itemConfig) {
logger.warn(`item gallery : item ${item} config not found !`)
return []
}
const galleryGlob = (productConfig.gallery || {})[assetSlug]?.glob || IMAGES_GLOB
const mediaPath = `${root}/${item}/${assetPath}/`
if (!exists(mediaPath)) {
logger.warn(`item gallery : item ${item} media path not found ${mediaPath}!`)
return []
}
const galleryGlob = (itemConfig.gallery || {})[assetSlug]?.glob || ASSETS_GLOB
let galleryFiles: any[] = files(mediaPath, galleryGlob, {
cwd: mediaPath,
absolute: false,
@ -85,10 +108,28 @@ export const gallery = async (
}
if (!galleryFiles) {
logger.warn(`ProductGallery : Product ${product} media files not found ! ${mediaPath}`)
return
logger.warn(`gallery : ${item} media files not found ! ${mediaPath}`)
return []
}
const assetUrl = (filePath) => {
return sanitizeUri(ITEM_ASSET_URL(
{
assetPath,
filePath,
ITEM_REL: item,
...profile.variables
}
))
}
if (!ASSETS_LOCAL) {
galleryFiles = await pMap(galleryFiles, async (f) => (await default_filter(assetUrl(f))) ? f : null, { concurrency: 5 })
galleryFiles = galleryFiles.filter((f) => f !== null)
galleryFiles = default_sort(galleryFiles)
} else {
galleryFiles = galleryFiles.filter(default_filter_locale)
}
galleryFiles = default_sort(galleryFiles)
return await pMap(galleryFiles, async (file: string) => {
const parts = path.parse(file)
const filePath = path.join(mediaPath, file)
@ -123,21 +164,12 @@ export const gallery = async (
delete imageMeta.exif.icc
delete imageMeta.exif.xmp
delete imageMeta.exif.iptc
const assetUrl = (filePath) => {
return sanitizeUri(ITEM_ASSET_URL(
{
assetPath,
filePath,
ITEM_REL: product,
...profile.variables
}
))
}
const src = ASSETS_LOCAL ? filePath : await image_url(assetUrl(file))
const ret: GalleryImage =
{
name: path.parse(file).name,
url: await image_url(assetUrl(file)),
src: await image_url(assetUrl(file)),
src: src,
url: src,
meta: {
format: imageMeta.format,
width: imageMeta.width,
@ -187,7 +219,7 @@ export const toJsonLd = async (images: GalleryImage[], lang: string, contentUrl:
"@context": "https://schema.org",
"@type": "ImageObject",
"inLanguage": lang,
"contentUrl": contentUrl || image.src,
"contentUrl": contentUrl || image.src,
"url": contentUrl || image.src,
"name": image.title || image.name,
"description": image.description || image.alt || "",
@ -196,7 +228,7 @@ export const toJsonLd = async (images: GalleryImage[], lang: string, contentUrl:
"identifier": image.name,
"keywords": Array.isArray(image.keywords) ? image.keywords.join(", ") : image.keywords || ""
}
// Add GPS coordinates if available
if (image.gps && image.gps.lat && image.gps.lon) {
jsonLd["contentLocation"] = {

View File

@ -6,7 +6,7 @@ import { I18N_SOURCE_LANGUAGE } from 'config/config.js'
import { translate } from '@/base/i18n.js'
import { item_defaults } from '@/base/index.js'
import config from "../app/config.json" with { "type": "json" }
import config from "../config/config.json" with { "type": "json" }
const keywords = (keywords: string) => keywords.split(',').map(k => k.trim()).filter(Boolean);

View File

@ -1,13 +1,13 @@
---
import "../styles/flowbite.css"
import "../styles/global.css"
import "../styles/custom.scss"
import { LANGUAGES_PROD } from "config/config.js"
import config from "config/config.json"
import { plainify } from "../base/strings.js"
import config from "@/config/config.json"
import { plainify } from "@/base/strings.js"
import "../styles/global.css"
import "../styles/custom.scss"
import { AstroSeo } from "@astrolib/seo"
import { item_keywords } from '@/base/seo.js'
@ -55,7 +55,7 @@ const description = item?.description as string || config.metadata.description
const keywords = await item_keywords(item, Astro.currentLocale)
const tracking = config?.tracking?.googleAnalytics
const tracking = config?.tracking?.googleAnalytics || 'G-RW6Q6EG3J0'
---
<AstroSeo
@ -146,21 +146,10 @@ const tracking = config?.tracking?.googleAnalytics
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date())
gtag('config', {tracking})
gtag('config', "G-RW6Q6EG3J0")
</script>
)}
{ REDIRECT && <script>
const currentPath = window.location.pathname;
if (!/^\/[a-z]{2}(\/|$)/i.test(currentPath)) {
let language = navigator.language
language = language.split('-')[0]
language = "en"
window.location.href = `/${language}`;
}
</script>
<link rel="sitemap" href="/sitemap-index.xml" />
}

View File

@ -1,6 +1,7 @@
---
const { title, url, author, pubDate, description, image } = Astro.props;
import { Img } from "imagetools/components"
const { title, url, pubDate, description, image } = Astro.props;
import { DEFAULT_IMAGE_URL } from "@/config/config";
import { Img } from "imagetools/components";
---
<div
@ -8,15 +9,16 @@ import { Img } from "imagetools/components"
>
<dt>
<h2 class="uppercase group-hover:text-orange-500 text-balance">
<div
class="z-0 scale-95 transition group-hover:scale-100"
>
</div>
<div class="z-0 scale-95 transition group-hover:scale-100"></div>
<a href={url} title={title}>
<span class="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6"></span><span class="relative z-10">{title}</span><br/><span class="text-sm">{pubDate}</span></a>
<span class="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6"
></span><span class="relative z-10">{title}</span><br /><span
class="text-sm">{pubDate}</span
></a
>
<div class="">
<Img
src={image.url}
src={image.src || DEFAULT_IMAGE_URL}
alt={image.alt}
sizes={`
(min-width: 1024px) 33vw,
@ -24,9 +26,9 @@ import { Img } from "imagetools/components"
100vw
`}
attributes={{
img:{
class: "w-full p-2 rounded-2xl relative object-contain lg:mt-2"
}
img: {
class: "w-full p-2 rounded-2xl relative object-contain lg:mt-2",
},
}}
/>
</div>

View File

@ -1,24 +1,9 @@
---
import { Img } from "imagetools/components"
import config from "@/app/config.json"
---
<section>
<div class="py-2">
<div
class="flex flex-col text-center bg-white rounded-xl overflow-hidden relative shadow-md">
<Img
src={config.pages.home.hero}
alt=""
format={['avif']}
objectFit="contain"
breakpoints={[100,500,800,1200,1600]}
objectPosition="50% 50%",
attributes={{
img:{
class:"md:-bottom-32"
}
}}
/>
<div class="max-w-xl mx-auto relative">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance uppercase">

View File

@ -1,25 +1,10 @@
---
import { Img } from "imagetools/components"
---
<section>
<div class="py-2">
<div
class="flex flex-col p-4 pb-32 bg-white rounded-xl overflow-hidden relative">
<Img
class="w-full absolute md:-bottom-96 -left-96 -ml-48"
src="/products/5.jpeg"
alt=""
format="png"
breakpoints={[800, 1600]}
attributes={{
img: {
class: `w-full absolute md:-bottom-96 -left-96 -ml-48`,
},
link: {
fetchpriority: "high",
},
}}
/>
<div class="max-w-xl relative ml-auto">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance uppercase">

View File

@ -1,131 +0,0 @@
---
interface Props extends Omit<astroHTML.JSX.IframeHTMLAttributes, 'src' | 'srcdoc'> {
/**
* Pass `true` to embed using `www.youtube.com` instead of `www.youtube-nocookie.com`
*/
cookie?: boolean
/**
* YouTube IFrame Player API parameters
*
* Defaults to `{autoplay: 1}`, additional parameters will be merged into the defaults
* @see https://developers.google.com/youtube/player_parameters#Parameters
*/
embedParams?: EmbedParams
/**
* `loading` attribute for the thumbnail `<img>`
*
* Defaults to `"lazy"`
*/
loading?: 'eager' | 'lazy'
/**
* Pass `true` to omit the "Watch on YouTube" link - saves ~3.86 KB
*/
noLink?: boolean
/**
* Thumbnail image to use in the static embed
*
* Defaults to `"default"`, pass 1, 2 or 3 to use a screenshot from the video instead.
*/
thumbnail?: Thumbnail
/**
* Thumbnail resolution
*
* Defaults to `"sd"` (640x480)
*/
thumbnailRes?: ThumbnailRes
/**
* Title for the static embed
*/
title: string
/**
* 11-digit YouTube video id
*/
videoId: string
}
interface EmbedParams {
autoplay?: ToggleParam
cc_lang_pref?: string
cc_load_policy?: ToggleParam
color?: 'red' | 'white'
controls?: ToggleParam
disablekb?: ToggleParam
enablejsapi?: ToggleParam
end?: number
fs?: ToggleParam
hl?: string
iv_load_policy?: 1 | '1' | 3 | '3'
list?: string
listType?: 'playlist' | 'user_uploads'
loop?: ToggleParam
/** @deprecated has no effect, deprecated by YouTube on August 15 2023 */
modestbranding?: ToggleParam
origin?: string
playlist?: string
playslinline?: ToggleParam
rel?: ToggleParam
start?: number
widget_referrer?: string
}
type ToggleParam = 0 | '0' | 1 | '1'
type Thumbnail = 'default' | 1 | '1' | 2 | '2' | 3 | '3' | string
type ThumbnailRes = 120 | '120' | 'default' | 320 | '320' | 'medium' | 'mq' | 480 | '480' | 'high' | 'hq' | 640 | '640' | 'standard' | 'sd' | 1280 | '1280' | 'maxres'
const QUALITY_PREFIXES = {
120: '',
default: '',
320: 'mq',
medium: 'mq',
mq: 'mq',
480: 'hq',
high: 'hq',
hq: 'hq',
640: 'sd',
standard: 'sd',
sd: 'sd',
1280: 'maxres',
maxres: 'maxres',
}
let {
cookie = false,
embedParams,
loading = 'lazy',
noLink = false,
thumbnail = 'default',
thumbnailRes = 'sd',
title,
videoId,
...iframeAttributes
} = Astro.props as Props
let params: EmbedParams = {autoplay: 1, ...embedParams}
let embedQuery = Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
let embedUrl = `https://www.youtube${cookie ? '' : '-nocookie'}.com/embed${videoId ? `/${videoId}` : ''}${embedQuery ? `?${embedQuery}` : ''}`
let thumbnailUrl = !/^(default|1|2|3)$/.test(String(thumbnail)) ? thumbnail : `https://i.ytimg.com/vi/${videoId}/${QUALITY_PREFIXES[thumbnailRes]}${thumbnail}.jpg`
let linkSvg = `<svg viewBox="0 0 110 26"><use href="#a" style="stroke-width:2px;stroke:#000;stroke-opacity:.15;stroke-linejoin:round"/><path id="a" fill="#fff" d="M16.68.99c-3.13.04-9.66.17-11.69.69-1.49.4-2.59 1.6-2.99 3-.69 2.7-.68 8.31-.68 8.31S1.31 18.6 2 21.3c.39 1.5 1.59 2.6 2.99 3 2.69.7 13.4.68 13.4.68s10.7.01 13.4-.68c1.5-.4 2.59-1.6 2.99-3 .69-2.7.68-8.31.68-8.31s.11-5.61-.68-8.31c-.4-1.5-1.59-2.6-2.99-3C29.11.98 18.4.99 18.4.99s-.67-.01-1.71 0zm72.21.9v21.28h2.78l.31-1.37h.09c.3.5.71.88 1.21 1.18.5.3 1.08.4 1.68.4 1.1 0 1.99-.49 2.49-1.59.5-1.1.81-2.7.81-4.9v-2.4c0-1.6-.11-2.9-.31-3.9-.2-.89-.5-1.59-1-2.09-.5-.4-1.1-.59-1.9-.59-.59 0-1.18.19-1.68.49-.49.3-1.01.8-1.21 1.4V1.9h-3.28zm-49.99.78 3.9 13.9.18 6.71h3.31v-6.71l3.87-13.9h-3.37l-1.4 6.31c-.4 1.89-.71 3.19-.81 3.99h-.09c-.2-1.1-.51-2.4-.81-3.99l-1.37-6.31h-3.4zm29.59 0v2.71h3.4v17.9h3.28V5.38h3.4s0-2.71-.09-2.71h-9.99zM15 7.79l8.9 5.18-8.9 5.09V7.78zm89.4.09c-1.7 0-2.89.59-3.59 1.59-.69.99-.99 2.6-.99 4.9v2.59c0 2.2.3 3.9.99 4.9.7 1.1 1.8 1.59 3.5 1.59 1.4 0 2.38-.3 3.18-1 .7-.7 1.09-1.69 1.09-3.09v-.5l-2.9-.21c0 1-.08 1.6-.28 2-.1.4-.5.62-1 .62-.3 0-.61-.11-.81-.31-.2-.3-.3-.59-.4-1.09-.1-.5-.09-1.21-.09-2.21v-.78l5.71-.09v-2.62c0-1.6-.1-2.78-.4-3.68-.2-.89-.71-1.59-1.31-1.99-.7-.4-1.48-.59-2.68-.59zm-50.49.09c-1.09 0-2.01.18-2.71.68-.7.4-1.2 1.12-1.49 2.12-.3 1-.5 2.27-.5 3.87v2.21c0 1.5.1 2.78.4 3.78.2.9.7 1.62 1.4 2.12.69.5 1.71.68 2.81.78 1.19 0 2.08-.28 2.78-.68.69-.4 1.09-1.09 1.49-2.09.39-1 .49-2.3.49-3.9v-2.21c0-1.6-.2-2.87-.49-3.87-.3-.89-.8-1.62-1.49-2.12-.7-.5-1.58-.68-2.68-.68zm12.18.09v11.9c-.1.3-.29.48-.59.68-.2.2-.51.31-.81.31-.3 0-.58-.1-.68-.4-.1-.3-.18-.7-.18-1.4V8.16h-3.4v11.21c0 1.4.18 2.39.68 3.09.49.7 1.21 1 2.21 1 1.4 0 2.48-.69 3.18-2.09h.09l.31 1.78h2.59V8.16s-3.4 0-3.4-.09zm17.31 0v11.9c-.1.3-.29.48-.59.68-.2.2-.51.31-.81.31-.3 0-.58-.1-.68-.4-.1-.3-.21-.7-.21-1.4V8.16h-3.4v11.21c0 1.4.21 2.39.71 3.09.5.7 1.18 1 2.18 1 1.39 0 2.51-.69 3.21-2.09h.09l.28 1.78h2.62V8.16s-3.4 0-3.4-.09zm20.9 2.09c.4 0 .58.11.78.31.2.3.3.59.4 1.09.1.5.09 1.21.09 2.21v1.09h-2.5v-1.09c0-1 0-1.71.09-2.21 0-.4.11-.8.31-1 .2-.3.51-.4.81-.4zm-50.49.12c.5 0 .8.18 1 .68.19.5.28 1.3.28 2.4v4.68c0 1.1-.08 1.9-.28 2.4s-.5.68-1 .68-.79-.18-.99-.68c-.2-.5-.31-1.3-.31-2.4v-4.68c0-1.1.11-1.9.31-2.4s.49-.68.99-.68zm39.68.09c.3 0 .61.1.81.4.2.3.27.67.37 1.37.1.6.12 1.51.12 2.71l.09 1.9c0 1.1 0 1.99-.09 2.59-.1.6-.19 1.08-.49 1.28-.2.3-.5.4-.9.4-.3 0-.51-.08-.81-.18-.2-.1-.39-.29-.59-.59v-8.5c.1-.4.29-.7.59-1 .3-.3.6-.4.9-.4z"/></svg>`
let playButtonSvg = `<svg viewBox="0 0 68 48"><path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00"></path><path d="M 45,24 27,14 27,34" fill="#fff"></path></svg>`
let gradientStyle = `.gradient{width:100%;height:49px;padding-bottom:50px;position:absolute;top:0;background-repeat:repeat-x;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);pointer-events:none}`
let linkStyle = noLink ? '' : '.woyt{z-index:2;background:rgba(23,23,23,.8);border-bottom-right-radius:2px;border-top-right-radius:2px;bottom:5px;height:47px;position:absolute}.woyt-text{color:#fff;float:left;font:500 16px/16px "YouTube Noto",Roboto,Arial,Helvetica,sans-serif;margin-left:12px;margin-top:16px}.woyt-logo{float:right;height:16px;margin-left:9px;margin-right:12px;margin-top:16px;width:72px}'
let style = `<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%;background:#000}img{position:absolute;width:100%;top:0;bottom:0;margin:auto}.button{position:absolute;left:50%;top:50%;width:68px;height:48px;margin-left:-34px;margin-top:-24px}.top{position:absolute;top:18px;left:18px;right:18px;display:flex;flex-wrap:nowrap}.title{color:#fff;font-size:18px;white-space:nowrap;word-wrap:normal;text-shadow:0 0 2px rgba(0,0,0,.5);font-family:"YouTube Noto",Roboto,Arial,Helvetica,sans-serif;line-height:1.3;text-overflow:ellipsis;overflow:hidden}${gradientStyle}${linkStyle}</style>`
let link = noLink ? '' : `<a href="https://www.youtube.com/watch?v=${videoId}${params.start ? `&t=${params.start}s` : ''}" target="_blank" aria-label="Watch on YouTube" class="woyt"><div aria-hidden="true"><div class="woyt-text">Watch on</div><div class="woyt-logo">${linkSvg}</div></div></a>`
let srcdoc = `${style}${link}<a href="${embedUrl}"><img src="${thumbnailUrl}" alt="${title}" loading="${loading}"><div class="gradient"></div><div class="top"><div class="title">${title}</div></div><div class="button">${playButtonSvg}</div></a>`
---
<iframe
loading={loading}
src={embedUrl}
srcdoc={srcdoc}
title={title}
{...iframeAttributes}
allow="accelerometer;autoplay;encrypted-media;gyroscope;picture-in-picture"
allowfullscreen
frameborder="0"
style="width:100%;aspect-ratio:16/9"
/>

View File

@ -1,140 +0,0 @@
<section>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-2">
<div class="flex flex-col h-full">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance">
Contact us
</h1>
<p class="text-sm text-balance text-neutral-500">
Reach out to us for any inquiries or assistance with your product or
service, or to get in touch with us for any other inquiries.
</p>
</div>
<form class="col-span-2 bg-white p-4 rounded-xl">
<div class="grid gap-2 grid-cols-1 sm:grid-cols-2">
<div>
<label
for="first-name"
class="sr-only"
>First name</label
>
<input
type="text"
id="first-name"
name="first-name"
autocomplete="given-name"
placeholder="Your name"
aria-label="First name"
class="flex-auto rounded-xl font-mono border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 text-neutral-500 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>
<label
for="last-name"
class="sr-only"
>Last name</label
>
<input
type="text"
id="last-name"
name="last-name"
autocomplete="family-name"
placeholder="Your last name"
aria-label="Last name"
class="flex-auto rounded-xl font-mono border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 text-neutral-500 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 class="sm:col-span-2">
<label
for="company"
class="sr-only"
>Company</label
>
<input
type="text"
id="company"
name="company"
autocomplete="organization"
placeholder="Company name"
aria-label="Company"
class="flex-auto rounded-xl font-mono border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 text-neutral-500 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 class="sm:col-span-2">
<label
for="email"
class="sr-only"
>Email</label
>
<input
type="email"
id="email"
name="email"
autocomplete="email"
placeholder="Your best email!"
aria-label="Email"
class="flex-auto rounded-xl font-mono border-0 h-14 text-xs uppercase duration-300 px-3.5 py-2 text-neutral-500 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 class="sm:col-span-2">
<label
for="message"
class="sr-only"
>Message</label
>
<textarea
rows="12"
id="message"
name="message"
placeholder="Your message goes here..."
aria-label="Message"
class="flex-auto rounded-xl font-mono border-0 text-xs uppercase duration-300 px-3.5 py-2 text-neutral-500 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"
></textarea>
</div>
</div>
<div class="mt-2 text-right">
<button
type="submit"
title="link to your page"
aria-label="your label"
class="relative group overflow-hidden pl-4 font-mono h-14 flex space-x-6 items-center bg-orange-500 hover:bg-black duration-300 rounded-xl w-full justify-between">
<span class="relative uppercase text-xs text-white">Submit</span>
<div
aria-hidden="true"
class="w-12 text-white 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>
</section>

View File

@ -1,6 +1,6 @@
---
import Wrapper from "@/components/containers/Wrapper.astro"
import { footer_left, footer_right } from "@/app/navigation.js"
import { footer_left, footer_right } from "@/config/navigation.js"
import { ISO_LANGUAGE_LABELS } from "@polymech/i18n"
import {
LANGUAGES_PROD,

View File

@ -1,7 +1,7 @@
---
import Wrapper from "@/components/containers/Wrapper.astro";
import { I18N_SOURCE_LANGUAGE } from "@/app/config";
import { items } from "config/navigation.js";
import { I18N_SOURCE_LANGUAGE } from "@/config/config";
import { items } from "@/config/navigation.js";
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE;
const navItems = await items({ locale });
---

View File

@ -1,7 +1,7 @@
---
import config from "@/app/config.json"
import config from "@/config/config.json"
import { get } from "@/model/registry.js"
import { default_image } from "@/app/config.js"
import { default_image } from "config/config.js"
const { frontmatter } = Astro.props
const title = frontmatter?.title || config.site.title
@ -39,5 +39,4 @@ let data = itemData || {
}
}
---
<script type="application/ld+json" set:html={JSON.stringify(data, null, 2)} />

View File

@ -1,11 +0,0 @@
<section>
<div class="flex flex-col h-full ">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight uppercase text-balance">
Williamsburg, one - your next ecommerce theme for all your products.
</h1>
<p class="text-sm text-pretty text-neutral-500 mt-2">
Your next minimalist ecommerce theme for all your digital products.
</p>
</div>
</section>

View File

@ -1,13 +0,0 @@
<section>
<div class="flex flex-col p-4 text-center py-20">
<div class="max-w-xl mx-auto">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance uppercase">
Williamsburg, your next ecommerce theme for all your products.
</h1>
<p class="text-sm text-balance text-neutral-500">
Your next minimalist ecommerce theme for all your digital products.
</p>
</div>
</div>
</section>

View File

@ -1,13 +0,0 @@
<section>
<div class="flex flex-col p-4 text-center py-20">
<div class="max-w-xl mx-auto">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance uppercase">
Williamsburg, your next ecommerce theme for all your products.
</h1>
<p class="text-sm text-balance text-neutral-500">
Your next minimalist ecommerce theme for all your digital products.
</p>
</div>
</div>
</section>

View File

@ -2,7 +2,6 @@
import { Img } from "imagetools/components";
import Translate from "@/components/polymech/i18n.astro"
import { translate } from "@/base/i18n";
import { createMarkdownComponent, markdownToHtml } from "@/base/index.js";
import { I18N_SOURCE_LANGUAGE, IMAGE_SETTINGS } from "config/config.js"
interface Image {

View File

@ -1,5 +1,5 @@
---
import { DEFAULT_IMAGE_URL } from '@/app/config.js'
import { DEFAULT_IMAGE_URL } from 'config/config.js'
import { Picture } from "imagetools/components"
import { image_url } from "@/base/media.js"
@ -19,4 +19,4 @@ const {
const srcSafe = await image_url(src, fallback)
---
<Picture class="" src={srcSafe} alt={alt} {...rest} />
<Picture src={srcSafe} alt={alt} {...rest} />

View File

@ -2,11 +2,10 @@
import JSX from "./jsx.astro"
import { sync as read } from "@polymech/fs/read"
import { sync as exists } from "@polymech/fs/exists"
import {} from "@polymech/cache"
import { run, OptionsSchema, IKBotTask } from "@polymech/kbot-d";
import { render } from "@/base";
import { renderMarkup } from "@/model/component.js";
import { createMarkdownComponent } from "@/base/index.js";
const { ...rest } = Astro.props;
const promptContent = ((await Astro.slots.render("default")) as string) || "";
@ -73,9 +72,7 @@ const renderers = {
jsx: async (content: string) => {},
html: async (content: string) => {},
md: async (str: string) => {
const markup = (await renderMarkup(str, options, "test.md")) || { html: "failed md" }
const ret = await render(markup.html)
return ret
return createMarkdownComponent(str)
},
astro: async (content: string) => {
// const compiled = await compile(content)

View File

@ -1,17 +1,16 @@
---
import { renderMarkup } from "@/model/component.js";
import { render, component } 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 { } from "@/base/i18n.js";
import { ASSET_URL } from "config/config.js";
import { fromMarkdown } from "mdast-util-from-markdown";
import { toString } from "mdast-util-to-string";
import { toMarkdown } from "mdast-util-to-markdown";
import { visit } from "unist-util-visit";
import { Root, Image } from "mdast";
// https://github.com/syntax-tree/mdast-util-mdxjs-esm?tab=readme-ov-file#when-to-use-this
// https://github.com/syntax-tree/mdast-util-to-markdown?tab=readme-ov-file#list-of-extensions
// https://chatgpt.com/c/67ba55c7-4c04-8001-b26b-bfa3a89aafb1
import { toMarkdown } from "mdast-util-to-markdown";
import { visit } from "unist-util-visit";
import { Root, RootContent, Heading, Image } from "mdast";
import { Img } from "imagetools/components";
interface Props {
markdown: string;
@ -40,10 +39,8 @@ const processImageUrls = (content: string, data: Record<string, string>) => {
return markup;
}
const ReadmeContent = await component(
const ReadmeContent = await createMarkdownComponent(
processImageUrls(markdown, data),
Astro.currentLocale,
{},
)
---

View File

@ -21,6 +21,7 @@ const title_i18n = await translate(title, I18N_SOURCE_LANGUAGE, locale);
href={id}
class="inline-block p-4 border-b-2
border-transparent rounded-t-lg
text-xs
hover:text-gray-600
hover:border-gray-300
dark:hover:text-gray-300"

View File

@ -1,187 +0,0 @@
---
const pricingPlans = [
{
name: "Individual",
monthlyPrice: "149",
annualPrice: "100",
description: "For freelancers and independent designers",
features: [
"Unlimited access to all design assets",
"Good plan for a freelancer and solo designer",
"Support for basic web development",
"Work on up to 3 product design projects",
"Only one user per account",
"Commercial use",
],
unavailableFeatures: [
"Licensed for teams, startups, agencies and corporates",
"Several users per account",
"Commercial use",
],
},
{
name: "Startup",
monthlyPrice: "249",
annualPrice: "300",
description: " Best choice for any size team or agency",
features: [
"Everything included in the Individual plan",
"Licensed for teams, startups, agencies and corporates",
"Several users per account",
"Commercial use",
],
unavailableFeatures: [
"Missing advanced 3D design tools",
"Without premium system collaboration",
"Standard support",
],
},
{
name: "Company",
monthlyPrice: "50",
annualPrice: "600",
description: "For businesses aiming to lead in innovation.",
features: [
"Everything included in the Individual plan",
"Everything included in the Startup plan",
"Unlimited licensed for teams, startups, agencies and corporates",
"Unlimited users per account",
"Commercial use",
],
unavailableFeatures: [],
},
];
---
<section x-data="{annual: false}">
<div
class="flex flex-col gap-12 h-full justify-between p-4 text-center pt-20">
<div class="max-w-xl mx-auto">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance">
Become a member today and unlock unlimited access.
</h1>
<p class="text-sm text-balance text-neutral-500">
Enroll in our membership program today at a discounted rate and enjoy
full access to our incredible lineup of products.
</p>
<div class="max-w-sm mx-auto">
<div
class="overflow-hidden inline-flex mt-6 z-0 h-14 rounded-lg p-0.5 w-full bg-white">
<button
class="text-xs text-black w-full block font-medium px-8 py-2 transition rounded-lg border border-transparent"
@click="annual = false"
:class="annual == false ? 'bg-neutral-100 border text-black ' : ''"
type="button"
>Monthly</button
>
<button
class="text-xs text-black w-full block font-medium px-8 py-2 transition rounded-lg border border-transparent"
@click="annual = true"
:class="annual == true ? 'bg-neutral-100 border text-black' : ''"
type="button"
>Annual ( save 25% )</button
>
</div>
</div>
</div>
</div>
<div class="grid lg:grid-cols-3 gap-2 py-2">
{
pricingPlans.map((plan) => (
<>
<div class="flex flex-col h-full bg-white p-4 rounded-xl">
<div class=" h-full flex flex-col ">
<div>
<div>
<div class="flex justify-between items-center">
<h3
id="tier-essential"
class="text-lg text-neutral-600 font-mono tracking-tight uppercase">
{plan.name}
</h3>
<p class="flex items-baseline gap-x-1 text-lg text-neutral-600 font-mono tracking-tight">
<span>
<span x-show="!annual">${plan.monthlyPrice}</span>
<span x-show="annual">${plan.annualPrice}</span>
</span>
<span class="text-xs">
/m
<span
x-show="annual"
style="display: none;">
(billed annually)
</span>
</span>
</p>
</div>
<p class="mt-4 text-sm text-neutral-500">
{plan.description}
</p>
</div>
<div class="mt-8">
<button
type="button"
title="link to your page"
aria-label="your label"
class="relative group overflow-hidden pl-4 font-mono h-14 flex space-x-6 items-center bg-orange-500 hover:bg-black duration-300 rounded-xl w-full justify-between">
<span class="relative uppercase text-xs text-white">
Add to cart
</span>
<div
aria-hidden="true"
class="w-12 text-white 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"
/>
</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"
/>
</svg>
</div>
</div>
</button>
</div>
</div>
<ul class="mt-6 text-xs space-y-1 font-mono uppercase text-neutral-500">
{plan.features.map((feature) => (
<li class="inline-flex items-start gap-3 ">
<span>+</span>
{feature}
</li>
))}
{plan.unavailableFeatures.map((feature) => (
<li class="inline-flex items-start gap-3 opacity-60">
<span></span> {feature}
</li>
))}
</ul>
</div>
</div>
</>
))
}
</div>
</section>

View File

@ -1,65 +0,0 @@
import { translate } from "@/base/i18n.js"
import config from "config/config.json"
}
/**
* Process and combine keywords from multiple sources, translate them, and trigger an optional callback
*
* @param options - Configuration for processing keywords
* @param options.itemConfig - The frontmatter or item configuration object
* @param options.sourceLanguage - The source language for translation
* @param options.targetLocale - The target locale for translation
* @param options.additionalKeywords - Extra keywords to add to the processed list
* @param options.onKeywordsProcessed - Optional callback that receives the processed keywords array
* @returns Object containing the processed keywords as both an array and comma-separated string
*/
export async function processKeywords({
itemConfig = {},
sourceLanguage,
targetLocale,
additionalKeywords = [],
onKeywordsProcessed
}: {
itemConfig?: Record<string, any>;
sourceLanguage: string;
targetLocale: string;
additionalKeywords?: string[];
onKeywordsProcessed?: (keywords: string[]) => void;
}) {
let systemKeywords = "";
if (itemConfig.PRODUCT_ROOT) {
const defaultsJson = await item_defaults(itemConfig.PRODUCT_ROOT);
const defaults: Record<string, string> = await import('@polymech/fs/read').then(m => m.sync(defaultsJson, 'json')) || {};
// Extract keywords from different sources
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
const configKeywords = (config.metadata?.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
const itemKeywords = (itemConfig.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
// Combine and deduplicate all keywords
const allKeywords = Array.from(new Set([
...defaultsKeywords,
...configKeywords,
...itemKeywords,
...additionalKeywords
])).join(',');
// Translate the keywords
systemKeywords = await translate(allKeywords, sourceLanguage, targetLocale);
}
// Final processing and deduplication
const keywordsArray = [...new Set([itemConfig.name, ...systemKeywords.split(','), ...additionalKeywords])].filter(Boolean);
const keywords = keywordsArray.join(',');
// Call the callback with the processed keywords if it exists
if (typeof onKeywordsProcessed === 'function') {
onKeywordsProcessed(keywordsArray);
}
return {
keywordsArray,
keywords
};
}

View File

@ -1,21 +0,0 @@
import { processKeywords } from '@/base/index.js'
const item_config = frontmatter as any || {}
// Process keywords using the extracted function
import { item_defaults, processKeywords } from '@/base/index.js'
itemConfig: item_config,
sourceLanguage: I18N_SOURCE_LANGUAGE,
targetLocale: locale,
additionalKeywords,
onKeywordsProcessed
})
// Process keywords using the extracted function
const item_config = frontmatter as any || {}
const { keywords, keywordsArray } = await processKeywords({
itemConfig: item_config,
sourceLanguage: I18N_SOURCE_LANGUAGE,
targetLocale: locale,
additionalKeywords,
onKeywordsProcessed
})

View File

@ -1,15 +1,16 @@
---
import { default_image } from "@/app/config.js";
import Img from "@/components/polymech/image.astro";
import { default_image } from "config/config.js";
import Translate from "@/components/polymech/i18n.astro";
import { Img } from "imagetools/components"
const { title, url, price, model, selected = false } = Astro.props;
const thumbnail = model?.assets?.renderings[0]?.url || default_image();
const classes = `group relative bg-white overflow-hidden group rounded-xl ${selected ? "ring-2 ring-orange-500" : ""}`
const thumbnail = model?.assets?.renderings[0]?.src || default_image();
const classes = `group relative bg-white overflow-hidden group rounded-xl ${selected ? "ring-2 ring-orange-500" : ""}`;
---
<div class={classes}>
<div class="p-4 overflow-hidden group-hover:opacity-75 duration-300 transition-all">
<div
class="p-4 overflow-hidden group-hover:opacity-75 duration-300 transition-all"
>
<a href={url} title={title} aria-label={title}>
<Img
src={thumbnail}
@ -30,7 +31,7 @@ const classes = `group relative bg-white overflow-hidden group rounded-xl ${sele
<Translate>{title}</Translate>
</a>
</h3>
<p class="absolute top-4 right-4">{price}</p>
<p class="top-4 right-4"></p>
<p class="mt-1"></p>
</div>
</div>

View File

@ -1,260 +0,0 @@
<section>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-2">
<div class="flex flex-col">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance">
Style guide
</h1>
<p class="text-sm text-balance text-neutral-500 mt-2">
Follow the style guide to create a consistent and professional look for
your website
</p>
</div>
<div class="lg:col-span-2">
<p class="text-lg text-neutral-600 font-mono tracking-tight text-balance">
Typography / Inter · IBM PLex Mono
</p>
<div class="mt-2">
<div>
<div class="space-y-2 py-3">
<div class="grid grid-cols-1 items-start lg:grid-cols-2">
<div><span class="text-black">Headers</span></div>
<div>
<span class="text-lg text-neutral-600 font-mono tracking-tight"
>Headers</span
>
</div>
</div>
<div class="grid grid-cols-1 items-start lg:grid-cols-2">
<div><span class="text-black">Link</span></div>
<div>
<a
href="#_"
class="text-sm hover:text-orange-600 text-neutral-500"
>Link text</a
>
</div>
</div>
<div class="grid grid-cols-1 items-start lg:grid-cols-2">
<div>
<span class="text-sm text-neutral-500">Paragraph</span>
</div>
<div>
<span class="text-sm text-neutral-600"
>I am so happy, my dear friend, so absorbed in the exquisite
sense of mere tranquil existence, that I neglect my talents.</span
>
</div>
</div>
<div class="grid grid-cols-1 items-start lg:grid-cols-2">
<div><span class="text-black">Caption</span></div>
<div>
<span class="text-netral-500 text-xs"
>Picture about somethign</span
>
</div>
</div>
<div class="grid grid-cols-1 items-start lg:grid-cols-2">
<div><span class="prose-styles">Code</span></div>
<div>
<code class="font-mono">tailwind.config.js</code>
</div>
</div>
<div class="grid grid-cols-1 items-start lg:grid-cols-2">
<div><span class="text-black">List</span></div>
<div>
<ul
class="text-neutral-600 list-inside space-y-3 list-disc"
role="list">
<ul
class="text-xs space-y-1 font-mono uppercase text-neutral-500"
role="list">
<li>Access to premium posts</li>
<li>Weekly newsletters</li>
<li>Simple, secure card payment</li>
<li>No Advertising</li>
<li>Special discounts</li>
</ul>
</ul>
</div>
</div>
</div>
</div>
</div>
<p
class="text-lg mt-12 text-neutral-600 font-mono tracking-tight text-balance">
Color palette
</p>
<div class="grid gap-2 grid-cols-2 lg:grid-cols-3 mt-2">
<div class="space-y-2">
<div class="lg:justify-center p-6 h-14 bg-white rounded-lg"></div>
</div>
<div>
<div class="lg:justify-center p-6 h-14 rounded-t-lg bg-orange-50">
</div>
<div class="lg:justify-center p-6 h-14 bg-orange-100"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-200"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-300"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-400"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-500"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-600"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-700"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-800"></div>
<div class="lg:justify-center p-6 h-14 bg-orange-900"></div>
<div class="lg:justify-center p-6 h-14 rounded-b-lg bg-orange-950">
</div>
</div>
<div>
<div class="lg:justify-center p-6 h-14 rounded-t-lg bg-neutral-50">
</div>
<div class="lg:justify-center p-6 h-14 bg-neutral-100"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-200"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-300"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-400"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-500"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-600"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-700"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-800"></div>
<div class="lg:justify-center p-6 h-14 bg-neutral-900"></div>
<div class="lg:justify-center p-6 h-14 rounded-b-lg bg-neutral-950">
</div>
</div>
</div>
<p
class="text-lg mt-12 text-neutral-600 font-mono tracking-tight text-balance">
Buttons
</p>
<div class="flex flex-wrap gap-2 mt-6">
<button
type="submit"
title="link to your page"
aria-label="your label"
class="relative group overflow-hidden pl-4 font-mono h-14 flex space-x-6 items-center bg-orange-500 hover:bg-black duration-300 rounded-xl justify-between">
<span class="relative uppercase text-xs text-white">Primary</span>
<div
aria-hidden="true"
class="w-12 text-white 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 justify-between text-xs text-white font-mono h-14 flex space-x-6 items-center bg-black hover:bg-neutral-200 hover:text-orange-600 duration-300 rounded-xl">
<span class="relative uppercase text-xs">Secondary</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 justify-between text-xs text-orange-600 font-mono 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">Secondary</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>
</div>
</div>
</section>

View File

@ -1,13 +1,8 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro"
import Wrapper from "@/components/containers/Wrapper.astro"
import { getCollection } from "astro:content"
import KBot from "@/components/polymech/kbot.astro"
const locale = Astro.currentLocale
const store = `${locale}/store/`
import Map from "@/components/polymech/map.astro"
const mapOptions = {
zoom: 9,
api_key: "AIzaSyCHsscCXksisHKMnUihOxl2X1mKny-qrqk"

View File

@ -20,7 +20,7 @@
"paths": {
"@/*": ["src/*"],
"site/*": ["src/*"],
"config/*": ["src/site/*"]
"config/*": ["src/config/*"]
}
},
"include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro"],