stuff
This commit is contained in:
parent
9a6fc75b4c
commit
7ff02e2ea9
@ -7,6 +7,7 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./base/*": "./dist/base/*",
|
||||
"./components/*": "./src/components/*"
|
||||
},
|
||||
"files": [
|
||||
|
||||
273
packages/polymech/src/base/specs.ts
Normal file
273
packages/polymech/src/base/specs.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import { parse } from 'node-xlsx'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
import pkg from 'showdown'
|
||||
const { Converter } = pkg
|
||||
|
||||
export const md2html = (content) => {
|
||||
let converter = new Converter({ tables: true });
|
||||
converter.setOption('literalMidWordUnderscores', 'true');
|
||||
return converter.makeHtml(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef MarkdownTableOptions
|
||||
* @property {string|null|Array.<string|null|undefined>} [align]
|
||||
* @property {boolean} [padding=true]
|
||||
* @property {boolean} [delimiterStart=true]
|
||||
* @property {boolean} [delimiterStart=true]
|
||||
* @property {boolean} [delimiterEnd=true]
|
||||
* @property {boolean} [alignDelimiters=true]
|
||||
* @property {(value: string) => number} [stringLength]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a table from a matrix of strings.
|
||||
*
|
||||
* from : https://github.com/wooorm/markdown-table/blob/main/index.js
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param {Array.<Array.<string|null|undefined>>} table
|
||||
* @param {MarkdownTableOptions} [options]
|
||||
* @returns {string}
|
||||
*/
|
||||
export const markdownTable = (table, options: any = {}) => {
|
||||
|
||||
const align = (options.align || []).concat()
|
||||
const stringLength = options.stringLength || defaultStringLength
|
||||
/** @type {Array<number>} Character codes as symbols for alignment per column. */
|
||||
const alignments: number[] = []
|
||||
/** @type {Array<Array<string>>} Cells per row. */
|
||||
const cellMatrix: string[][] = []
|
||||
/** @type {Array<Array<number>>} Sizes of each cell per row. */
|
||||
const sizeMatrix: number[][] = []
|
||||
/** @type {Array<number>} */
|
||||
const longestCellByColumn: number[] = []
|
||||
let mostCellsPerRow = 0
|
||||
let rowIndex = -1
|
||||
|
||||
// This is a superfluous loop if we don’t align delimiters, but otherwise we’d
|
||||
// do superfluous work when aligning, so optimize for aligning.
|
||||
while (++rowIndex < table.length) {
|
||||
/** @type {Array<string>} */
|
||||
const row: string[] = []
|
||||
/** @type {Array<number>} */
|
||||
const sizes: number[] = []
|
||||
let columnIndex = -1
|
||||
|
||||
if (table[rowIndex].length > mostCellsPerRow) {
|
||||
mostCellsPerRow = table[rowIndex].length
|
||||
}
|
||||
|
||||
while (++columnIndex < table[rowIndex].length) {
|
||||
const cell = serialize(table[rowIndex][columnIndex])
|
||||
|
||||
if (options.alignDelimiters !== false) {
|
||||
const size = stringLength(cell)
|
||||
sizes[columnIndex] = size
|
||||
|
||||
if (
|
||||
longestCellByColumn[columnIndex] === undefined ||
|
||||
size > longestCellByColumn[columnIndex]
|
||||
) {
|
||||
longestCellByColumn[columnIndex] = size
|
||||
}
|
||||
}
|
||||
|
||||
row.push(cell)
|
||||
}
|
||||
|
||||
cellMatrix[rowIndex] = row
|
||||
sizeMatrix[rowIndex] = sizes
|
||||
}
|
||||
|
||||
// Figure out which alignments to use.
|
||||
let columnIndex = -1
|
||||
|
||||
if (typeof align === 'object' && 'length' in align) {
|
||||
while (++columnIndex < mostCellsPerRow) {
|
||||
alignments[columnIndex] = toAlignment(align[columnIndex])
|
||||
}
|
||||
} else {
|
||||
const code = toAlignment(align)
|
||||
|
||||
while (++columnIndex < mostCellsPerRow) {
|
||||
alignments[columnIndex] = code
|
||||
}
|
||||
}
|
||||
|
||||
// Inject the alignment row.
|
||||
columnIndex = -1
|
||||
/** @type {Array<string>} */
|
||||
const row: string[] = []
|
||||
/** @type {Array<number>} */
|
||||
const sizes: number[] = []
|
||||
|
||||
while (++columnIndex < mostCellsPerRow) {
|
||||
const code = alignments[columnIndex]
|
||||
let before = ''
|
||||
let after = ''
|
||||
|
||||
if (code === 99 /* `c` */) {
|
||||
before = ':'
|
||||
after = ':'
|
||||
} else if (code === 108 /* `l` */) {
|
||||
before = ':'
|
||||
} else if (code === 114 /* `r` */) {
|
||||
after = ':'
|
||||
}
|
||||
|
||||
// There *must* be at least one hyphen-minus in each alignment cell.
|
||||
let size =
|
||||
options.alignDelimiters === false
|
||||
? 1
|
||||
: Math.max(
|
||||
1,
|
||||
longestCellByColumn[columnIndex] - before.length - after.length
|
||||
)
|
||||
|
||||
const cell = before + '-'.repeat(size) + after
|
||||
|
||||
if (options.alignDelimiters !== false) {
|
||||
size = before.length + size + after.length
|
||||
|
||||
if (size > longestCellByColumn[columnIndex]) {
|
||||
longestCellByColumn[columnIndex] = size
|
||||
}
|
||||
|
||||
sizes[columnIndex] = size
|
||||
}
|
||||
|
||||
row[columnIndex] = cell
|
||||
}
|
||||
|
||||
// Inject the alignment row.
|
||||
cellMatrix.splice(1, 0, row)
|
||||
sizeMatrix.splice(1, 0, sizes)
|
||||
|
||||
rowIndex = -1
|
||||
/** @type {Array<string>} */
|
||||
const lines: string[] = []
|
||||
|
||||
while (++rowIndex < cellMatrix.length) {
|
||||
const row = cellMatrix[rowIndex]
|
||||
const sizes = sizeMatrix[rowIndex]
|
||||
columnIndex = -1
|
||||
/** @type {Array<string>} */
|
||||
const line: string[] = []
|
||||
|
||||
while (++columnIndex < mostCellsPerRow) {
|
||||
const cell = row[columnIndex] || ''
|
||||
let before = ''
|
||||
let after = ''
|
||||
|
||||
if (options.alignDelimiters !== false) {
|
||||
const size =
|
||||
longestCellByColumn[columnIndex] - (sizes[columnIndex] || 0)
|
||||
const code = alignments[columnIndex]
|
||||
|
||||
if (code === 114 /* `r` */) {
|
||||
before = ' '.repeat(size)
|
||||
} else if (code === 99 /* `c` */) {
|
||||
if (size % 2) {
|
||||
before = ' '.repeat(size / 2 + 0.5)
|
||||
after = ' '.repeat(size / 2 - 0.5)
|
||||
} else {
|
||||
before = ' '.repeat(size / 2)
|
||||
after = before
|
||||
}
|
||||
} else {
|
||||
after = ' '.repeat(size)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.delimiterStart !== false && !columnIndex) {
|
||||
line.push('|')
|
||||
}
|
||||
|
||||
if (
|
||||
options.padding !== false &&
|
||||
// Don’t add the opening space if we’re not aligning and the cell is
|
||||
// empty: there will be a closing space.
|
||||
!(options.alignDelimiters === false && cell === '') &&
|
||||
(options.delimiterStart !== false || columnIndex)
|
||||
) {
|
||||
line.push(' ')
|
||||
}
|
||||
|
||||
if (options.alignDelimiters !== false) {
|
||||
line.push(before)
|
||||
}
|
||||
|
||||
line.push(cell)
|
||||
|
||||
if (options.alignDelimiters !== false) {
|
||||
line.push(after)
|
||||
}
|
||||
|
||||
if (options.padding !== false) {
|
||||
line.push(' ')
|
||||
}
|
||||
|
||||
if (
|
||||
options.delimiterEnd !== false ||
|
||||
columnIndex !== mostCellsPerRow - 1
|
||||
) {
|
||||
line.push('|')
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(
|
||||
options.delimiterEnd === false
|
||||
? line.join('').replace(/ +$/, '')
|
||||
: line.join('')
|
||||
)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null|undefined} [value]
|
||||
* @returns {string}
|
||||
*/
|
||||
function serialize(value) {
|
||||
return value === null || value === undefined ? '' : String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @returns {number}
|
||||
*/
|
||||
function defaultStringLength(value) {
|
||||
return value.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null|undefined} value
|
||||
* @returns {number}
|
||||
*/
|
||||
function toAlignment(value) {
|
||||
const code = typeof value === 'string' ? value.codePointAt(0) : 0
|
||||
|
||||
return code === 67 /* `C` */ || code === 99 /* `c` */
|
||||
? 99 /* `c` */
|
||||
: code === 76 /* `L` */ || code === 108 /* `l` */
|
||||
? 108 /* `l` */
|
||||
: code === 82 /* `R` */ || code === 114 /* `r` */
|
||||
? 114 /* `r` */
|
||||
: 0
|
||||
}
|
||||
|
||||
|
||||
export const specs = (path: string) => {
|
||||
if (!path || !exists(path)) {
|
||||
return '';
|
||||
} else {
|
||||
let data = parse(path) as any;
|
||||
data[0].data = data[0].data.filter((d) => !!d.length);
|
||||
data = markdownTable(data[0].data);
|
||||
const ret = md2html(data);
|
||||
return ret
|
||||
}
|
||||
}
|
||||
7
packages/polymech/src/components/Default.astro
Normal file
7
packages/polymech/src/components/Default.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
import { createComponent } from "astro/runtime/server/astro-component.js";
|
||||
import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
|
||||
const DefaultComponent = createComponent(() => renderTemplate( unescapeHTML(`<div />`), []));
|
||||
---
|
||||
<DefaultComponent />
|
||||
|
||||
229
packages/polymech/src/components/GalleryK.astro
Normal file
229
packages/polymech/src/components/GalleryK.astro
Normal file
@ -0,0 +1,229 @@
|
||||
---
|
||||
import { Img } from "imagetools/components";
|
||||
import Translate from "./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 {
|
||||
alt: string
|
||||
src: string
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
images: Image[];
|
||||
id?: string;
|
||||
gallerySettings?: {
|
||||
SIZES_REGULAR?: string;
|
||||
SIZES_THUMB?: string;
|
||||
SIZES_LARGE?: string;
|
||||
SHOW_TITLE?: boolean;
|
||||
SHOW_DESCRIPTION?: boolean;
|
||||
};
|
||||
lightboxSettings?: {
|
||||
SIZES_REGULAR?: string;
|
||||
SIZES_THUMB?: string;
|
||||
SIZES_LARGE?: string;
|
||||
SHOW_TITLE?: boolean;
|
||||
SHOW_DESCRIPTION?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const { images, gallerySettings = {}, lightboxSettings = {} } = Astro.props;
|
||||
|
||||
const mergedGallerySettings = {
|
||||
SIZES_REGULAR: gallerySettings.SIZES_REGULAR || IMAGE_SETTINGS.GALLERY.SIZES_REGULAR,
|
||||
SIZES_THUMB: gallerySettings.SIZES_THUMB || IMAGE_SETTINGS.GALLERY.SIZES_THUMB,
|
||||
SHOW_TITLE: gallerySettings.SHOW_TITLE ?? IMAGE_SETTINGS.GALLERY.SHOW_TITLE,
|
||||
SHOW_DESCRIPTION: gallerySettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.GALLERY.SHOW_DESCRIPTION,
|
||||
};
|
||||
|
||||
const mergedLightboxSettings = {
|
||||
SIZES_LARGE: lightboxSettings.SIZES_LARGE || IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE,
|
||||
SHOW_TITLE: lightboxSettings.SHOW_TITLE ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_TITLE,
|
||||
SHOW_DESCRIPTION: lightboxSettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_DESCRIPTION,
|
||||
};
|
||||
const locale = Astro.currentLocale || "en";
|
||||
|
||||
---
|
||||
|
||||
<div
|
||||
x-data={`
|
||||
{
|
||||
open: false,
|
||||
currentIndex: 0,
|
||||
total: ${images.length},
|
||||
lightboxLoaded: false,
|
||||
touchStartX: 0,
|
||||
touchEndX: 0,
|
||||
minSwipeDistance: 50,
|
||||
isSwiping: false,
|
||||
images: ${JSON.stringify(images)},
|
||||
handleSwipe() {
|
||||
if (!this.isSwiping) return;
|
||||
const swipeDistance = this.touchEndX - this.touchStartX;
|
||||
if (Math.abs(swipeDistance) >= this.minSwipeDistance) {
|
||||
if (swipeDistance > 0 && this.currentIndex > 0) {
|
||||
// Swiped right, show previous image
|
||||
this.currentIndex--;
|
||||
} else if (swipeDistance < 0 && this.currentIndex < this.total - 1) {
|
||||
// Swiped left, show next image
|
||||
this.currentIndex++;
|
||||
}
|
||||
}
|
||||
this.isSwiping = false;
|
||||
},
|
||||
preloadAndOpen() {
|
||||
if (this.isSwiping) return;
|
||||
this.lightboxLoaded = false;
|
||||
let img = new Image();
|
||||
img.src = this.images[this.currentIndex].src;
|
||||
img.onload = () => {
|
||||
this.lightboxLoaded = true;
|
||||
this.open = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
`}
|
||||
@keydown.escape.window="open = false"
|
||||
@keydown.window="if(open){
|
||||
if($event.key === 'ArrowRight' && currentIndex < total - 1){
|
||||
currentIndex++;
|
||||
lightboxLoaded = false;
|
||||
preloadAndOpen();
|
||||
} else if($event.key === 'ArrowLeft' && currentIndex > 0){
|
||||
currentIndex--;
|
||||
lightboxLoaded = false;
|
||||
preloadAndOpen();
|
||||
}
|
||||
}"
|
||||
class="product-gallery"><div class="flex flex-col h-full">
|
||||
<!-- Main Image (with swipe functionality) -->
|
||||
<div
|
||||
class="flex-1 p-1 flex items-center justify-center cursor-pointer rounded-lg"
|
||||
@click="preloadAndOpen()"
|
||||
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
|
||||
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
|
||||
@touchcancel="isSwiping = false;"
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index} class="w-full h-full flex items-center justify-center">
|
||||
<Img
|
||||
src={image.src}
|
||||
alt={image.alt,I18N_SOURCE_LANGUAGE,locale}
|
||||
objectFit="contain"
|
||||
format="avif"
|
||||
placeholder="blurred"
|
||||
sizes={mergedGallerySettings.SIZES_REGULAR}
|
||||
attributes={{
|
||||
img: { class: "main-image p-1 rounded-lg max-h-[60vh] aspect-square" }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<!-- Image Info -->
|
||||
{ (mergedGallerySettings.SHOW_TITLE || mergedGallerySettings.SHOW_DESCRIPTION) && (
|
||||
<div class="text-center py-4">
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index}>
|
||||
{ mergedGallerySettings.SHOW_TITLE && ( <h2 id="imageTitle" class="text-xl font-bold">{image.title}</h2>)}
|
||||
{ mergedGallerySettings.SHOW_DESCRIPTION && (<p id="imageDescription" class="text-gray-600"><Translate>{ image.description}</Translate></p>)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<!-- Thumbnails -->
|
||||
<div class="p-1 overflow-x-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-200">
|
||||
<div class="flex p-2 mt-2 ml-2 mr-2 gap-2 items-center justify-center">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
x-on:click={`currentIndex = ${index};`}
|
||||
:class={`currentIndex === ${index} ? 'ring-2 ring-orange-500' : ''`}
|
||||
class="thumbnail thumbnail-btn flex-shrink rounded-lg"
|
||||
>
|
||||
<Img
|
||||
src={image.src}
|
||||
objectFit="cover"
|
||||
format="avif"
|
||||
placeholder="blurred"
|
||||
sizes={mergedGallerySettings.SIZES_THUMB}
|
||||
class="w-32 h-32 p-1 object-contain rounded hover:ring-2 hover:ring-blue-500"
|
||||
alt={translate(image.alt,I18N_SOURCE_LANGUAGE,locale)}
|
||||
attributes={{
|
||||
img: {
|
||||
class: "w-32 h-32 rounded-lg hover:ring-2 hover:ring-blue-500 thumbnail-img aspect-square",
|
||||
}
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Lightbox Modal (with swipe functionality) -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
:class="{ 'lightbox': !lightboxLoaded }"
|
||||
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center lightbox"
|
||||
>
|
||||
<div
|
||||
class="relative max-w-full max-h-full"
|
||||
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
|
||||
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
|
||||
@touchcancel="isSwiping = false;"
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index}>
|
||||
<Img
|
||||
src={image.src}
|
||||
alt={image.alt}
|
||||
placeholder="blurred"
|
||||
format="avif"
|
||||
objectFit="contain"
|
||||
sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE}
|
||||
attributes={{
|
||||
img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main" }
|
||||
}}
|
||||
/>
|
||||
{ (mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) && (
|
||||
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 m-[8px] max-h-[32vh] p-2 text-white bg-black/50 rounded-lg" style="width: 90%;">
|
||||
{ mergedLightboxSettings.SHOW_TITLE && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
|
||||
{ mergedLightboxSettings.SHOW_DESCRIPTION && (<p><Translate>{image.description}</Translate></p>)} </div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
x-on:click="open = false"
|
||||
class="absolute top-0 right-0 text-white text-2xl p-4 m-[8px] bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<!-- Navigation Buttons -->
|
||||
<button
|
||||
x-show="currentIndex > 0"
|
||||
x-on:click="currentIndex--;"
|
||||
class="absolute left-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
aria-label="Previous"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<button
|
||||
x-show="currentIndex < total - 1"
|
||||
x-on:click="currentIndex++; "
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
aria-label="Next"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
21
packages/polymech/src/components/i18n.astro
Normal file
21
packages/polymech/src/components/i18n.astro
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
|
||||
import { translate, IOptions } from '@/base/i18n.js'
|
||||
|
||||
export interface Props extends IOptions {
|
||||
language?: string,
|
||||
clazz?:string
|
||||
}
|
||||
|
||||
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)
|
||||
---
|
||||
<div data-widget="polymech.i18n" class={clazz}>
|
||||
{translatedText}
|
||||
</div>
|
||||
43
packages/polymech/src/components/specs.astro
Normal file
43
packages/polymech/src/components/specs.astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
import * as path from "path"
|
||||
import { sync as fileExists } from "@polymech/fs/exists"
|
||||
|
||||
import { specs } from "@/base/specs.js"
|
||||
|
||||
import { render, logger } from "@/base/index.js"
|
||||
import { translateSheets } from "@/base/i18n.js"
|
||||
import { I18N_SOURCE_LANGUAGE, LANGUAGES, PRODUCT_SPECS } from "config/config.js"
|
||||
|
||||
import DefaultComponent from "./Default.astro"
|
||||
|
||||
const { frontmatter: data } = Astro.props
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE
|
||||
|
||||
const specs_table = async (relPath) =>
|
||||
{
|
||||
let specsPath = path.join(PRODUCT_SPECS(relPath));
|
||||
if (!fileExists(specsPath)) {
|
||||
logger.debug(`No specs found for ${specsPath}`);
|
||||
return null;
|
||||
}
|
||||
if (locale !== I18N_SOURCE_LANGUAGE && LANGUAGES.includes(locale)) {
|
||||
const i18n = await translateSheets(relPath, locale);
|
||||
if (!i18n) {
|
||||
logger.debug(`No i18n found for ${relPath} : ${locale}`);
|
||||
} else {
|
||||
specsPath = i18n
|
||||
}
|
||||
}
|
||||
return specs(specsPath)
|
||||
}
|
||||
const tableHTML = await specs_table(data.rel)
|
||||
let SpecsComponent
|
||||
if (tableHTML) {
|
||||
SpecsComponent = await render(tableHTML)
|
||||
} else {
|
||||
SpecsComponent = DefaultComponent
|
||||
}
|
||||
---
|
||||
<div class="bg-white rounded-xl specs specs-table">
|
||||
<SpecsComponent />
|
||||
</div>
|
||||
33
packages/polymech/src/components/tab-button.astro
Normal file
33
packages/polymech/src/components/tab-button.astro
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
import { translate } from "@/base/i18n.js";
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/config.js";
|
||||
import { slugify } from "@/base/strings.js"
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
const { title = "title" } = Astro.props;
|
||||
|
||||
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);
|
||||
---
|
||||
|
||||
<li>
|
||||
<a
|
||||
data-tabs-target={view}
|
||||
href={id}
|
||||
class="inline-block p-4 border-b-2
|
||||
border-transparent rounded-t-lg
|
||||
hover:text-gray-600
|
||||
hover:border-gray-300
|
||||
dark:hover:text-gray-300"
|
||||
id={slug}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="settings"
|
||||
aria-selected="false">{title_i18n}</a
|
||||
>
|
||||
</li>
|
||||
19
packages/polymech/src/components/tab-content.astro
Normal file
19
packages/polymech/src/components/tab-content.astro
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
import { slugify } from "@/base/strings.js";
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
const { title = "title" } = Astro.props;
|
||||
const slug = `${slugify(title)}-tab-content`;
|
||||
const label = `${slug}-tab`;
|
||||
const view = `${slugify(title)}-view`;
|
||||
---
|
||||
|
||||
<div
|
||||
class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800 content"
|
||||
id={view}
|
||||
role="tabpanel"
|
||||
aria-labelledby={label}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
65
packages/polymech/src/components/tab-item.astro
Normal file
65
packages/polymech/src/components/tab-item.astro
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
import { translate } from '@/base/i18n.js';
|
||||
import { I18N_SOURCE_LANGUAGE } from "config/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
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
||||
.replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
clazz?: string;
|
||||
src?: string; // optional
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'title',
|
||||
clazz = 'unstyled',
|
||||
src,
|
||||
} = Astro.props;
|
||||
|
||||
// Compute the slug from the original title
|
||||
const id = slugify(title);
|
||||
|
||||
// Then retrieve the user's locale and translate the title
|
||||
const locale = Astro.currentLocale;
|
||||
const title_i18n = await translate(title, I18N_SOURCE_LANGUAGE, locale);
|
||||
---
|
||||
|
||||
<button
|
||||
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>
|
||||
|
||||
<div slot="panel" class={clazz}>
|
||||
{src ? (
|
||||
<div class="iframe-container">
|
||||
<iframe src={src}></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<slot />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 ratio; adjust if needed */
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
10
packages/polymech/src/components/tabs.astro
Normal file
10
packages/polymech/src/components/tabs.astro
Normal file
@ -0,0 +1,10 @@
|
||||
<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-tab"
|
||||
data-tabs-toggle="#default-tab-content"
|
||||
role="tablist"
|
||||
>
|
||||
<slot/>
|
||||
</ul>
|
||||
</div>
|
||||
@ -19,7 +19,8 @@
|
||||
"declaration": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"config/*": ["src/app/*"]
|
||||
"config/*": ["src/app/*"],
|
||||
"components/*": ["src/components/*"]
|
||||
}
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user