meno-astro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/chunks/chunk-IVGZB6XN.js +2921 -0
- package/dist/chunks/chunk-IVGZB6XN.js.map +7 -0
- package/dist/chunks/chunk-LEMVUKVG.js +90 -0
- package/dist/chunks/chunk-LEMVUKVG.js.map +7 -0
- package/dist/chunks/chunk-PQQAY42O.js +91 -0
- package/dist/chunks/chunk-PQQAY42O.js.map +7 -0
- package/dist/chunks/chunk-ZYQNHI3W.js +16 -0
- package/dist/chunks/chunk-ZYQNHI3W.js.map +7 -0
- package/dist/lib/components/BaseLayout.astro +53 -0
- package/dist/lib/components/Embed.astro +24 -0
- package/dist/lib/components/Link.astro +19 -0
- package/dist/lib/components/LocaleList.astro +75 -0
- package/dist/lib/components/index.js +14 -0
- package/dist/lib/components/index.js.map +7 -0
- package/dist/lib/dialect/index.js +1385 -0
- package/dist/lib/dialect/index.js.map +7 -0
- package/dist/lib/index.js +141 -0
- package/dist/lib/index.js.map +7 -0
- package/dist/lib/integration/index.js +56 -0
- package/dist/lib/integration/index.js.map +7 -0
- package/dist/lib/runtime/localeMiddleware.js +16 -0
- package/dist/lib/runtime/localeMiddleware.js.map +7 -0
- package/package.json +36 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ../core/lib/shared/types/cms.ts
|
|
2
|
+
var IRREGULAR_PLURALS = {
|
|
3
|
+
categories: "category",
|
|
4
|
+
children: "child",
|
|
5
|
+
people: "person",
|
|
6
|
+
men: "man",
|
|
7
|
+
women: "woman",
|
|
8
|
+
feet: "foot",
|
|
9
|
+
teeth: "tooth",
|
|
10
|
+
geese: "goose",
|
|
11
|
+
mice: "mouse",
|
|
12
|
+
indices: "index",
|
|
13
|
+
vertices: "vertex",
|
|
14
|
+
matrices: "matrix"
|
|
15
|
+
};
|
|
16
|
+
function singularize(collection) {
|
|
17
|
+
if (IRREGULAR_PLURALS[collection]) {
|
|
18
|
+
return IRREGULAR_PLURALS[collection];
|
|
19
|
+
}
|
|
20
|
+
if (collection.endsWith("ies")) {
|
|
21
|
+
return collection.slice(0, -3) + "y";
|
|
22
|
+
}
|
|
23
|
+
if (collection.endsWith("ses") || collection.endsWith("xes") || collection.endsWith("zes") || collection.endsWith("ches") || collection.endsWith("shes")) {
|
|
24
|
+
return collection.slice(0, -2);
|
|
25
|
+
}
|
|
26
|
+
return collection.endsWith("s") ? collection.slice(0, -1) : collection;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ../core/lib/shared/types/prefetch.ts
|
|
30
|
+
var DEFAULT_PREFETCH_CONFIG = {
|
|
31
|
+
enabled: false,
|
|
32
|
+
defaultStrategy: "hover",
|
|
33
|
+
hoverDebounce: 65,
|
|
34
|
+
maxCacheSize: 10,
|
|
35
|
+
cacheTTL: 5 * 60 * 1e3,
|
|
36
|
+
// 5 minutes
|
|
37
|
+
respectSaveData: true,
|
|
38
|
+
slowConnectionThreshold: 2
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ../core/lib/shared/types/variables.ts
|
|
42
|
+
var VARIABLE_GROUPS = [
|
|
43
|
+
{ value: "font-family", label: "Font Family" },
|
|
44
|
+
{ value: "font-size", label: "Font Size" },
|
|
45
|
+
{ value: "font-weight", label: "Font Weight" },
|
|
46
|
+
{ value: "line-height", label: "Line Height" },
|
|
47
|
+
{ value: "letter-spacing", label: "Letter Spacing" },
|
|
48
|
+
{ value: "margin", label: "Margin" },
|
|
49
|
+
{ value: "padding", label: "Padding" },
|
|
50
|
+
{ value: "gap", label: "Gap" },
|
|
51
|
+
{ value: "size", label: "Size" },
|
|
52
|
+
{ value: "position", label: "Position" },
|
|
53
|
+
{ value: "border-radius", label: "Border Radius" },
|
|
54
|
+
{ value: "border-width", label: "Border Width" },
|
|
55
|
+
{ value: "outline", label: "Outline" },
|
|
56
|
+
{ value: "shadow", label: "Shadow" },
|
|
57
|
+
{ value: "filter", label: "Filter" },
|
|
58
|
+
{ value: "duration", label: "Duration" },
|
|
59
|
+
{ value: "aspect-ratio", label: "Aspect Ratio" },
|
|
60
|
+
{ value: "opacity", label: "Opacity" },
|
|
61
|
+
{ value: "z-index", label: "Z-Index" },
|
|
62
|
+
{ value: "text-align", label: "Text Align" },
|
|
63
|
+
{ value: "other", label: "Other" }
|
|
64
|
+
];
|
|
65
|
+
var VARIABLE_GROUP_VALUES = VARIABLE_GROUPS.map((g) => g.value);
|
|
66
|
+
var VARIABLE_TYPES = [
|
|
67
|
+
{ value: "fontSize", label: "Font Size" },
|
|
68
|
+
{ value: "padding", label: "Padding" },
|
|
69
|
+
{ value: "margin", label: "Margin" },
|
|
70
|
+
{ value: "gap", label: "Gap" },
|
|
71
|
+
{ value: "borderRadius", label: "Border Radius" },
|
|
72
|
+
{ value: "size", label: "Width / Height" },
|
|
73
|
+
{ value: "none", label: "None" }
|
|
74
|
+
];
|
|
75
|
+
var VARIABLE_TYPE_VALUES = VARIABLE_TYPES.map((t) => t.value);
|
|
76
|
+
|
|
77
|
+
// ../core/lib/shared/types/comment.ts
|
|
78
|
+
var COMMENT_STATUSES = [
|
|
79
|
+
"open",
|
|
80
|
+
"in-progress",
|
|
81
|
+
"ready-for-review",
|
|
82
|
+
"resolved",
|
|
83
|
+
"closed"
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
singularize,
|
|
88
|
+
COMMENT_STATUSES
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=chunk-LEMVUKVG.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../core/lib/shared/types/cms.ts", "../../../core/lib/shared/types/prefetch.ts", "../../../core/lib/shared/types/variables.ts", "../../../core/lib/shared/types/comment.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * CMS Types\n * Defines schema structure and content items for CMS collections\n */\n\n/** Field types supported by CMS */\nexport type CMSFieldType =\n | 'string' // Single-line text (auto-localizable when project has multiple locales)\n | 'text' // Multi-line text (auto-localizable when project has multiple locales)\n | 'rich-text' // Rich text with formatting (auto-localizable when project has multiple locales)\n | 'number' // Numeric value\n | 'boolean' // True/false\n | 'image' // Image URL/path\n | 'file' // File URL/path (any file type)\n | 'date' // ISO date string\n | 'select' // Single selection from options\n | 'reference'; // Reference to another collection item\n\n/** Single field definition in a schema */\nexport interface CMSFieldDefinition {\n type: CMSFieldType;\n required?: boolean;\n default?: unknown;\n label?: string; // Display label in editor\n description?: string; // Help text\n options?: string[]; // For 'select' type\n accept?: string; // For 'file' type - MIME filter (e.g. \"application/pdf\", \"*/*\")\n collection?: string; // For 'reference' type - target collection ID\n multiple?: boolean; // For 'reference' type - allows array of IDs\n}\n\n/** Data delivery strategy for client-side CMS data */\nexport type CMSClientDataStrategy = 'auto' | 'inline' | 'static';\n\n/**\n * Configuration for exposing CMS data to client-side JavaScript.\n * Enables dynamic filtering/sorting on static websites.\n */\nexport interface CMSClientDataConfig {\n /** Enable client-side data exposure */\n enabled: boolean;\n /**\n * Data delivery strategy:\n * - 'auto': Choose based on collection size (inline < threshold, static >= threshold)\n * - 'inline': Embed JSON directly in HTML (best for < 500 items)\n * - 'static': Generate separate JSON files (best for large collections)\n */\n strategy: CMSClientDataStrategy;\n /** Threshold for auto strategy switch (default: 500 items) */\n threshold?: number;\n /**\n * Fields to expose to client (for security/size control).\n * System fields (_id, _url, _filename) are always included.\n * Empty array or undefined = include all fields.\n */\n fields?: string[];\n}\n\n/** CMS Schema - defines collection structure (embedded in page meta.cms) */\nexport interface CMSSchema {\n /** Unique identifier for the collection */\n id: string;\n /** Display name for the collection */\n name: string;\n /** Which field to use for URL slug */\n slugField: string;\n /** URL pattern, e.g., \"/blog/{{slug}}\" */\n urlPattern: string;\n /** Field definitions */\n fields: Record<string, CMSFieldDefinition>;\n /** Client-side data configuration for dynamic filtering */\n clientData?: CMSClientDataConfig;\n}\n\n/**\n * CMS Item - actual content entry (stored as individual JSON file).\n *\n * Identity model:\n * - `_id` is the canonical identifier. For new items written by the editor, it\n * equals the on-disk filename's stem (slug-shaped, human-readable). For\n * legacy items it may be a custom value (e.g. `\"post-001\"`) that differs\n * from the filename \u2014 those continue to work; reference resolution accepts\n * either `_id` or the on-disk filename as a key.\n * - `_filename` is a legacy alias kept for back-compat. The provider's\n * `normalizeItem` always sets it on read to the on-disk filename's stem, so\n * internal file-routing code can rely on it. New items do not need to\n * persist `_filename` to disk \u2014 write only `_id`.\n * - `_slug` is the oldest identifier alias, fully deprecated. Use `_id`.\n */\nexport interface CMSItem {\n /** Canonical identifier. For new items, equals the on-disk filename's stem. */\n _id: string;\n /**\n * @deprecated Use _id. Kept for backward compatibility.\n * Derived from the filename on disk.\n */\n _slug?: string;\n /**\n * @deprecated Legacy alias for _id. Always equals the on-disk filename's\n * stem; populated automatically by the file-system provider on read.\n * Internal file-routing code reads this for legacy items where the on-disk\n * filename differs from `_id`. New items do not need to write this to disk.\n */\n _filename?: string;\n /** ISO timestamp when item was created */\n _createdAt?: string;\n /**\n * Locale codes where this item is hidden from the live site.\n * undefined or [] = visible for all locales (backward compatible).\n * ['fr', 'de'] = French and German are hidden, other locales visible.\n *\n * NOTE: this is locale-level visibility (\"Hidden\" in the UI), NOT the WIP\n * draft-version concept. The draft version of an item is stored in a\n * sibling `{filename}.draft.json` file. The legacy `_draftLocales` name\n * is preserved for backward compatibility.\n */\n _draftLocales?: string[];\n /**\n * Computed URL path for this item based on collection's urlPattern.\n * Available when item is returned with URL context (e.g., in CMSList or template pages).\n * Example: \"/blog/my-post\" for urlPattern \"/blog/{{slug}}\"\n */\n _url?: string;\n /**\n * Transient flag set when an item was loaded from a `{filename}.draft.json`\n * file (i.e. it is the draft version, not the published one). Never persisted.\n */\n _isDraft?: boolean;\n /**\n * Transient flag set on a published item when a draft sibling exists.\n * Used by the Studio item list to render a \"Has draft\" badge. Never persisted.\n */\n _hasDraft?: boolean;\n /** User-defined fields based on schema */\n [field: string]: unknown;\n}\n\n/**\n * Bundle of both versions of a CMS item, returned by editor APIs.\n * - `published` is omitted for draft-only items (new items not yet published).\n * - `draft` is omitted when there are no pending changes.\n */\nexport interface CMSItemVersions {\n published?: CMSItem;\n draft?: CMSItem;\n}\n\n/** CMS Collection - schema + items */\nexport interface CMSCollection {\n schema: CMSSchema;\n items: CMSItem[];\n}\n\n/** Resolved CMS route match */\nexport interface CMSRouteMatch {\n collection: string;\n slug: string;\n item: CMSItem;\n pagePath: string; // Path to the template page file\n}\n\n/** Filter operators for CMS queries */\nexport type CMSFilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in';\n\n/** Single filter condition */\nexport interface CMSFilterCondition {\n field: string;\n operator?: CMSFilterOperator; // defaults to 'eq'\n value: unknown;\n}\n\n/** Sort configuration */\nexport interface CMSSortConfig {\n field: string;\n order?: 'asc' | 'desc'; // defaults to 'asc'\n}\n\n/** CMSList query configuration */\nexport interface CMSListQuery {\n collection: string;\n filter?: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>;\n sort?: CMSSortConfig | CMSSortConfig[];\n limit?: number;\n offset?: number;\n /** When set, excludes items that are draft for this locale */\n excludeDraftLocale?: string;\n}\n\n/**\n * @deprecated CMSListNode is now replaced by ListNode with sourceType: 'collection'.\n * This type alias is kept for backward compatibility during migration.\n * Import ListNode from '../registry/nodeTypes/ListNodeType' instead.\n */\nexport type CMSListNode = import('../registry/nodeTypes/ListNodeType').ListNode;\n\n/**\n * Configuration for nested CMS list placeholders.\n * Serialized to data-cms-config attribute for client-side hydration\n * when parent cms-list has emitTemplate: true.\n */\nexport interface NestedCMSListConfig {\n /** Collection to query */\n collection: string;\n /** Variable name for item in templates */\n itemAs?: string;\n /** Direct item IDs or template expression for referenced items */\n items?: string | string[];\n /** Filter conditions (may contain template expressions like {{parent.field}}) */\n filter?: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>;\n /** Sort configuration */\n sort?: CMSSortConfig | CMSSortConfig[];\n /** Maximum number of items to return */\n limit?: number;\n /** Number of items to skip */\n offset?: number;\n /** Exclude the current CMS item from the list */\n excludeCurrentItem?: boolean;\n}\n\n/**\n * Item context for CMS List template rendering\n * Available when rendering children inside a CMSList\n * @deprecated Use TemplateContext with named contexts instead\n */\nexport interface ItemContext {\n /** Type discriminator (optional for backward compat) */\n _type?: 'item';\n /** Current CMS item data */\n item: CMSItem;\n /** Zero-based index of current item */\n itemIndex: number;\n /** True if this is the first item */\n itemFirst: boolean;\n /** True if this is the last item */\n itemLast: boolean;\n}\n\n/**\n * Template context for CMS List rendering with named contexts.\n * Supports nested lists where parent contexts are preserved.\n *\n * Example:\n * ```\n * {\n * _type: 'template',\n * post: { _id: '1', title: 'Hello' },\n * postIndex: 0,\n * postFirst: true,\n * postLast: false,\n * author: { _id: '2', name: 'John' },\n * authorIndex: 0,\n * authorFirst: true,\n * authorLast: true,\n * // Legacy context also included for backward compatibility\n * item: { _id: '2', name: 'John' },\n * itemIndex: 0,\n * itemFirst: true,\n * itemLast: true\n * }\n * ```\n */\nexport interface TemplateContext {\n /** Type discriminator for distinguishing from ItemContext */\n _type: 'template';\n [key: string]: CMSItem | number | boolean | 'template' | undefined;\n}\n\n// ============================================================================\n// Singularization Helper\n// ============================================================================\n\n/** Lookup table for irregular plurals */\nconst IRREGULAR_PLURALS: Record<string, string> = {\n categories: 'category',\n children: 'child',\n people: 'person',\n men: 'man',\n women: 'woman',\n feet: 'foot',\n teeth: 'tooth',\n geese: 'goose',\n mice: 'mouse',\n indices: 'index',\n vertices: 'vertex',\n matrices: 'matrix',\n};\n\n/**\n * Convert a collection name to its singular form for use as item variable name.\n * Used when itemAs is not explicitly specified.\n *\n * @example\n * singularize('posts') // 'post'\n * singularize('categories') // 'category'\n * singularize('author') // 'author'\n */\nexport function singularize(collection: string): string {\n // Check irregular plurals first\n if (IRREGULAR_PLURALS[collection]) {\n return IRREGULAR_PLURALS[collection];\n }\n // Handle -ies -> -y (e.g., \"stories\" -> \"story\")\n if (collection.endsWith('ies')) {\n return collection.slice(0, -3) + 'y';\n }\n // Handle -es for words ending in s, x, z, ch, sh\n if (collection.endsWith('ses') || collection.endsWith('xes') ||\n collection.endsWith('zes') || collection.endsWith('ches') ||\n collection.endsWith('shes')) {\n return collection.slice(0, -2);\n }\n // Simple rule: remove trailing 's' if present\n return collection.endsWith('s') ? collection.slice(0, -1) : collection;\n}\n\n/**\n * Describes a location where a CMS item is referenced.\n * Used for delete warnings and reference tracking.\n */\nexport interface ReferenceLocation {\n /** Collection containing the reference */\n collection: string;\n /** Item ID that has the reference */\n itemId: string;\n /** Slug for display (optional) */\n itemSlug?: string;\n /** Filename for display (optional) */\n itemFilename?: string;\n /** Field name containing the reference */\n fieldName: string;\n}\n\n/** Check if a CMS item is in draft state for a specific locale */\nexport function isItemDraftForLocale(item: CMSItem, locale: string): boolean {\n return item._draftLocales?.includes(locale) === true;\n}\n\n/** Check if a CMS item is fully published (not draft for any locale) */\nexport function isItemFullyPublished(item: CMSItem): boolean {\n return !item._draftLocales || item._draftLocales.length === 0;\n}\n", "/**\n * Prefetch configuration types for Astro-style link prefetching\n *\n * Supports multiple strategies:\n * - hover: Prefetch on mouseenter/focus (default)\n * - tap: Prefetch on mousedown/touchstart (conservative)\n * - viewport: Prefetch when link enters viewport\n * - load: Prefetch all links on page load\n */\n\n/**\n * Prefetch strategies matching Astro's approach\n */\nexport type PrefetchStrategy = 'hover' | 'tap' | 'viewport' | 'load';\n\n/**\n * Prefetch configuration options\n */\nexport interface PrefetchConfig {\n /** Whether prefetching is enabled (default: false) */\n enabled: boolean;\n\n /** Default strategy for all links (default: 'hover') */\n defaultStrategy: PrefetchStrategy;\n\n /** Debounce time for hover events in ms (default: 65) */\n hoverDebounce: number;\n\n /** Maximum number of cached pages (LRU cache, default: 10) */\n maxCacheSize: number;\n\n /** TTL for cached pages in ms (default: 5 minutes) */\n cacheTTL: number;\n\n /** Respect user's save-data preference (default: true) */\n respectSaveData: boolean;\n\n /** Slow connection threshold in Mbps - fall back to tap (default: 2) */\n slowConnectionThreshold: number;\n}\n\n/**\n * Default prefetch configuration\n */\nexport const DEFAULT_PREFETCH_CONFIG: PrefetchConfig = {\n enabled: false,\n defaultStrategy: 'hover',\n hoverDebounce: 65,\n maxCacheSize: 10,\n cacheTTL: 5 * 60 * 1000, // 5 minutes\n respectSaveData: true,\n slowConnectionThreshold: 2,\n};\n\n/**\n * Cached page entry with timestamp for TTL management\n */\nexport interface CachedPageEntry {\n /** Parsed page data (ComponentNode tree) */\n data: unknown;\n /** Timestamp when cached */\n timestamp: number;\n /** Page-specific components */\n components?: Record<string, unknown>;\n}\n\n/**\n * Connection quality info for adaptive prefetching\n * Uses Network Information API when available\n */\nexport interface ConnectionInfo {\n /** User has enabled data saver mode */\n saveData: boolean;\n /** Effective connection type */\n effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | 'unknown';\n /** Estimated downlink speed in Mbps */\n downlink: number;\n}\n", "/**\n * CSS Variables Types\n * Defines types for the variables.json configuration file\n */\n\n/**\n * Category type for a CSS variable\n * Maps to responsiveScales categories for automatic responsive scaling\n * 'none' means no responsive scaling is applied\n */\nexport type VariableType = 'fontSize' | 'padding' | 'margin' | 'gap' | 'borderRadius' | 'size' | 'none';\n\n/**\n * UI filtering group for a CSS variable.\n * Controls which variables appear in the picker based on the CSS property being edited.\n */\nexport type VariableGroup =\n | 'font-family' | 'font-size' | 'font-weight'\n | 'line-height' | 'letter-spacing'\n | 'margin' | 'padding' | 'gap'\n | 'size'\n | 'position'\n | 'border-radius' | 'border-width'\n | 'outline'\n | 'shadow'\n | 'filter'\n | 'duration'\n | 'aspect-ratio'\n | 'opacity' | 'z-index'\n | 'text-align'\n | 'other';\n\n/** Predefined variable groups for UI dropdowns */\nexport const VARIABLE_GROUPS: { value: VariableGroup; label: string }[] = [\n { value: 'font-family', label: 'Font Family' },\n { value: 'font-size', label: 'Font Size' },\n { value: 'font-weight', label: 'Font Weight' },\n { value: 'line-height', label: 'Line Height' },\n { value: 'letter-spacing', label: 'Letter Spacing' },\n { value: 'margin', label: 'Margin' },\n { value: 'padding', label: 'Padding' },\n { value: 'gap', label: 'Gap' },\n { value: 'size', label: 'Size' },\n { value: 'position', label: 'Position' },\n { value: 'border-radius', label: 'Border Radius' },\n { value: 'border-width', label: 'Border Width' },\n { value: 'outline', label: 'Outline' },\n { value: 'shadow', label: 'Shadow' },\n { value: 'filter', label: 'Filter' },\n { value: 'duration', label: 'Duration' },\n { value: 'aspect-ratio', label: 'Aspect Ratio' },\n { value: 'opacity', label: 'Opacity' },\n { value: 'z-index', label: 'Z-Index' },\n { value: 'text-align', label: 'Text Align' },\n { value: 'other', label: 'Other' },\n];\n\n/** All valid group string values */\nexport const VARIABLE_GROUP_VALUES: VariableGroup[] = VARIABLE_GROUPS.map(g => g.value);\n\n/** Maps CSS property names to variable groups */\nconst CSS_PROPERTY_TO_GROUP: Record<string, VariableGroup> = {\n 'font-family': 'font-family',\n 'font-size': 'font-size',\n 'font-weight': 'font-weight',\n 'line-height': 'line-height',\n 'letter-spacing': 'letter-spacing',\n 'text-align': 'text-align',\n 'opacity': 'opacity',\n 'z-index': 'z-index',\n 'top': 'position',\n 'right': 'position',\n 'bottom': 'position',\n 'left': 'position',\n 'inset': 'position',\n 'inset-block': 'position',\n 'inset-inline': 'position',\n 'inset-block-start': 'position',\n 'inset-block-end': 'position',\n 'inset-inline-start': 'position',\n 'inset-inline-end': 'position',\n 'box-shadow': 'shadow',\n 'text-shadow': 'shadow',\n 'filter': 'filter',\n 'backdrop-filter': 'filter',\n 'aspect-ratio': 'aspect-ratio',\n 'transition-duration': 'duration',\n 'animation-duration': 'duration',\n 'transition-delay': 'duration',\n 'animation-delay': 'duration',\n};\n\n/** Prefix-based mappings for CSS properties */\nconst CSS_PROPERTY_PREFIX_GROUPS: { prefix: string; group: VariableGroup }[] = [\n { prefix: 'margin', group: 'margin' },\n { prefix: 'padding', group: 'padding' },\n { prefix: 'gap', group: 'gap' },\n { prefix: 'row-gap', group: 'gap' },\n { prefix: 'column-gap', group: 'gap' },\n { prefix: 'width', group: 'size' },\n { prefix: 'height', group: 'size' },\n { prefix: 'min-width', group: 'size' },\n { prefix: 'max-width', group: 'size' },\n { prefix: 'min-height', group: 'size' },\n { prefix: 'max-height', group: 'size' },\n { prefix: 'border-radius', group: 'border-radius' },\n { prefix: 'border-top-left-radius', group: 'border-radius' },\n { prefix: 'border-top-right-radius', group: 'border-radius' },\n { prefix: 'border-bottom-left-radius', group: 'border-radius' },\n { prefix: 'border-bottom-right-radius', group: 'border-radius' },\n { prefix: 'border-width', group: 'border-width' },\n { prefix: 'border-top-width', group: 'border-width' },\n { prefix: 'border-right-width', group: 'border-width' },\n { prefix: 'border-bottom-width', group: 'border-width' },\n { prefix: 'border-left-width', group: 'border-width' },\n { prefix: 'outline-width', group: 'outline' },\n { prefix: 'outline-offset', group: 'outline' },\n];\n\n/**\n * Determines which variable group a CSS property belongs to.\n * Returns null if no group matches (show all variables).\n */\nexport function getGroupForProperty(prop: string): VariableGroup | null {\n // Normalize camelCase to kebab-case (e.g., \"fontFamily\" \u2192 \"font-family\")\n const normalized = prop.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);\n\n // Exact match first\n if (normalized in CSS_PROPERTY_TO_GROUP) {\n return CSS_PROPERTY_TO_GROUP[normalized];\n }\n // Prefix-based match (handles margin-top, padding-left, etc.)\n for (const { prefix, group } of CSS_PROPERTY_PREFIX_GROUPS) {\n if (normalized === prefix || normalized.startsWith(prefix + '-')) {\n return group;\n }\n }\n return null;\n}\n\n/**\n * A single CSS custom property definition\n */\nexport interface CSSVariable {\n /** Display name (e.g., \"H1 Font Size\") */\n name: string;\n /** Optional prop name alias for organizational purposes */\n prop_name?: string;\n /** CSS custom property name (e.g., \"--h1-fs\") */\n cssVar: string;\n /** Base value (e.g., \"48px\") */\n value: string;\n /** Category for responsive scaling */\n type: VariableType;\n /** Optional per-variable breakpoint scale overrides */\n scales?: Record<string, string>;\n /** Optional UI filtering group for the variable picker */\n group?: VariableGroup;\n}\n\n/**\n * Variables configuration file structure (variables.json)\n */\nexport interface VariablesConfig {\n variables: CSSVariable[];\n}\n\n/** Predefined variable scaling types for UI dropdowns */\nexport const VARIABLE_TYPES: { value: VariableType; label: string }[] = [\n { value: 'fontSize', label: 'Font Size' },\n { value: 'padding', label: 'Padding' },\n { value: 'margin', label: 'Margin' },\n { value: 'gap', label: 'Gap' },\n { value: 'borderRadius', label: 'Border Radius' },\n { value: 'size', label: 'Width / Height' },\n { value: 'none', label: 'None' },\n];\n\n/** All valid variable scaling type values */\nexport const VARIABLE_TYPE_VALUES: VariableType[] = VARIABLE_TYPES.map(t => t.value);\n\n/**\n * Returns an abbreviation for a variable group by taking the first letter of each word.\n * e.g. 'letter-spacing' -> 'ls', 'font-size' -> 'fs', 'spacing' -> 's'\n */\nexport function getGroupAbbreviation(group: string): string {\n return group.split('-').map(w => w[0] || '').join('');\n}\n\n/**\n * Infers a default scaling type from a variable group and optional CSS property.\n */\nexport function getDefaultScalingType(group: string, cssProperty?: string): VariableType {\n if (group === 'font-size') return 'fontSize';\n if (group === 'margin') return 'margin';\n if (group === 'padding') return 'padding';\n if (group === 'gap') return 'gap';\n if (group === 'border-radius') return 'borderRadius';\n if (group === 'size') return 'size';\n return 'none';\n}\n\n/**\n * Generates a short CSS variable name with collision detection.\n * e.g. name=\"Heading\", group=\"letter-spacing\" -> \"--h-ls\"\n * If taken, extends prefix: \"--he-ls\", \"--hea-ls\", etc.\n */\nexport function generateShortCssVar(\n name: string,\n group: string,\n existingVariables: CSSVariable[]\n): string {\n const trimmed = name.trim();\n if (!trimmed) return '--';\n if (!group) {\n // No group: just kebab-case the name\n const base = '--' + trimmed.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '');\n return ensureUnique(base, existingVariables);\n }\n\n const groupAbbr = getGroupAbbreviation(group);\n const kebabName = trimmed.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '');\n const words = kebabName.split('-').filter(Boolean);\n const existingNames = new Set(existingVariables.map(v => v.cssVar));\n\n // Try progressively longer prefixes from the name\n // Start with first letter of each word, then extend first word\n const initials = words.map(w => w[0] || '').join('');\n const initialCandidate = `--${initials}-${groupAbbr}`;\n if (!existingNames.has(initialCandidate)) return initialCandidate;\n\n // Extend first word progressively\n const firstWord = words[0] || '';\n for (let len = 2; len <= firstWord.length; len++) {\n const prefix = firstWord.slice(0, len) + (words.length > 1 ? words.slice(1).map(w => w[0] || '').join('') : '');\n const candidate = `--${prefix}-${groupAbbr}`;\n if (!existingNames.has(candidate)) return candidate;\n }\n\n // Full kebab name with group abbreviation\n const fullCandidate = `--${kebabName}-${groupAbbr}`;\n if (!existingNames.has(fullCandidate)) return fullCandidate;\n\n // Numeric suffix fallback\n let i = 2;\n while (existingNames.has(`${fullCandidate}-${i}`)) i++;\n return `${fullCandidate}-${i}`;\n}\n\nfunction ensureUnique(base: string, existingVariables: CSSVariable[]): string {\n const existingNames = new Set(existingVariables.map(v => v.cssVar));\n if (!existingNames.has(base)) return base;\n let i = 2;\n while (existingNames.has(`${base}-${i}`)) i++;\n return `${base}-${i}`;\n}\n", "/**\n * Comment / Pin Types\n *\n * Comments are Figma-style pins placed on components in the studio canvas.\n * One file per pin (thread embedded). Storage layout (status as top folder):\n * {projectRoot}/comments/<status>/<pageSlug>--<seq>--<titleSlug>.json\n * e.g. comments/open/blog__post--3--hero-is-too-dark.json\n *\n * Pins are an authoring-time concept \u2014 they live in studio only.\n */\n\n/** Workflow status. No custom statuses for MVP. */\nexport type CommentStatus =\n | 'open'\n | 'in-progress'\n | 'ready-for-review'\n | 'resolved'\n | 'closed';\n\nexport const COMMENT_STATUSES: readonly CommentStatus[] = [\n 'open',\n 'in-progress',\n 'ready-for-review',\n 'resolved',\n 'closed',\n] as const;\n\n/** Identifies the GitHub user who authored a thread entry. */\nexport interface CommentAuthor {\n /** GitHub login. `\"local\"` for non-electron / unauthenticated runs. */\n login: string;\n name?: string;\n avatarUrl?: string;\n}\n\n/**\n * Identity fingerprint for the anchored node \u2014 used to detect orphan pins\n * when the tree shifts. Mirrors the shape of `nodesMatch()` in NodeStore.\n */\nexport interface CommentNodeIdentity {\n /** `'component'` or `'node'` (HTML). */\n kind: 'component' | 'node';\n /** Component name (when kind=component) or HTML tag (when kind=node). */\n name: string;\n /** Editor-set label, used to disambiguate same-type siblings. */\n label?: string;\n}\n\n/**\n * Where the pin attaches on the page. nodePath is positional; nodeIdentity\n * is used at render time to detect that the pin has been orphaned (Phase 4).\n */\nexport interface CommentAnchor {\n /** Path array into the page tree, e.g. [0, 1, 2]. */\n nodePath: number[];\n /** Identity fingerprint for orphan detection. */\n nodeIdentity: CommentNodeIdentity;\n /** Horizontal offset inside the component's bounding box, 0..1. */\n offsetXPercent: number;\n /** Vertical offset inside the component's bounding box, 0..1. */\n offsetYPercent: number;\n /**\n * Breakpoint frame this pin was placed on (e.g. `'base'`, `'tablet'`,\n * `'mobile'`). Only set for pins created in Design Mode's multi-frame\n * canvas, so the pin is only shown on the frame where it was pinned.\n * When unset, the pin renders on every frame (single-frame / page mode,\n * or pins created before this field existed).\n */\n breakpoint?: string;\n}\n\n/** One message in a comment's thread (initial + replies). */\nexport interface CommentThreadEntry {\n id: string;\n author: CommentAuthor;\n /** ISO timestamp. */\n createdAt: string;\n text: string;\n /** When this entry transitioned the comment status, the target status. */\n statusChange: CommentStatus | null;\n}\n\n/** A pinned comment. */\nexport interface Comment {\n _id: string;\n _filename: string;\n /** Page path the pin lives on (e.g. `\"blog/post\"`). */\n _pagePath: string;\n /** Stable per-page badge number. Server assigns at create time. */\n _seq: number;\n /** ISO timestamps. */\n _createdAt: string;\n _updatedAt: string;\n /** Git HEAD SHA captured at create time, or null when unavailable. */\n _commitSha: string | null;\n anchor: CommentAnchor;\n status: CommentStatus;\n thread: CommentThreadEntry[];\n}\n"],
|
|
5
|
+
"mappings": ";AAgRA,IAAM,oBAA4C;AAAA,EAChD,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AACZ;AAWO,SAAS,YAAY,YAA4B;AAEtD,MAAI,kBAAkB,UAAU,GAAG;AACjC,WAAO,kBAAkB,UAAU;AAAA,EACrC;AAEA,MAAI,WAAW,SAAS,KAAK,GAAG;AAC9B,WAAO,WAAW,MAAM,GAAG,EAAE,IAAI;AAAA,EACnC;AAEA,MAAI,WAAW,SAAS,KAAK,KAAK,WAAW,SAAS,KAAK,KACvD,WAAW,SAAS,KAAK,KAAK,WAAW,SAAS,MAAM,KACxD,WAAW,SAAS,MAAM,GAAG;AAC/B,WAAO,WAAW,MAAM,GAAG,EAAE;AAAA,EAC/B;AAEA,SAAO,WAAW,SAAS,GAAG,IAAI,WAAW,MAAM,GAAG,EAAE,IAAI;AAC9D;;;AC7QO,IAAM,0BAA0C;AAAA,EACrD,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,UAAU,IAAI,KAAK;AAAA;AAAA,EACnB,iBAAiB;AAAA,EACjB,yBAAyB;AAC3B;;;ACnBO,IAAM,kBAA6D;AAAA,EACxE,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,EAC7C,EAAE,OAAO,aAAa,OAAO,YAAY;AAAA,EACzC,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,EAC7C,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,EAC7C,EAAE,OAAO,kBAAkB,OAAO,iBAAiB;AAAA,EACnD,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,OAAO,OAAO,MAAM;AAAA,EAC7B,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,iBAAiB,OAAO,gBAAgB;AAAA,EACjD,EAAE,OAAO,gBAAgB,OAAO,eAAe;AAAA,EAC/C,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,gBAAgB,OAAO,eAAe;AAAA,EAC/C,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,cAAc,OAAO,aAAa;AAAA,EAC3C,EAAE,OAAO,SAAS,OAAO,QAAQ;AACnC;AAGO,IAAM,wBAAyC,gBAAgB,IAAI,OAAK,EAAE,KAAK;AA8G/E,IAAM,iBAA2D;AAAA,EACtE,EAAE,OAAO,YAAY,OAAO,YAAY;AAAA,EACxC,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,EACrC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,OAAO,OAAO,MAAM;AAAA,EAC7B,EAAE,OAAO,gBAAgB,OAAO,gBAAgB;AAAA,EAChD,EAAE,OAAO,QAAQ,OAAO,iBAAiB;AAAA,EACzC,EAAE,OAAO,QAAQ,OAAO,OAAO;AACjC;AAGO,IAAM,uBAAuC,eAAe,IAAI,OAAK,EAAE,KAAK;;;AChK5E,IAAM,mBAA6C;AAAA,EACxD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_I18N_CONFIG,
|
|
3
|
+
extractLocaleFromPath,
|
|
4
|
+
isI18nValue,
|
|
5
|
+
resolveI18nValue
|
|
6
|
+
} from "./chunk-IVGZB6XN.js";
|
|
7
|
+
import {
|
|
8
|
+
__require
|
|
9
|
+
} from "./chunk-ZYQNHI3W.js";
|
|
10
|
+
|
|
11
|
+
// lib/runtime/i18n.ts
|
|
12
|
+
var store;
|
|
13
|
+
var fallbackContext;
|
|
14
|
+
try {
|
|
15
|
+
const { AsyncLocalStorage } = __require("node:async_hooks");
|
|
16
|
+
store = new AsyncLocalStorage();
|
|
17
|
+
} catch {
|
|
18
|
+
store = void 0;
|
|
19
|
+
}
|
|
20
|
+
function runWithLocale(locale, config, fn) {
|
|
21
|
+
const ctx = { locale, config };
|
|
22
|
+
if (store) {
|
|
23
|
+
return store.run(ctx, fn);
|
|
24
|
+
}
|
|
25
|
+
const prev = fallbackContext;
|
|
26
|
+
fallbackContext = ctx;
|
|
27
|
+
try {
|
|
28
|
+
return fn();
|
|
29
|
+
} finally {
|
|
30
|
+
fallbackContext = prev;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getLocaleContext() {
|
|
34
|
+
if (store) return store.getStore();
|
|
35
|
+
return fallbackContext;
|
|
36
|
+
}
|
|
37
|
+
function localeFromAstro(astro, config) {
|
|
38
|
+
if (astro?.currentLocale) return astro.currentLocale;
|
|
39
|
+
const pathname = astro?.url?.pathname;
|
|
40
|
+
if (typeof pathname === "string") {
|
|
41
|
+
const { locale } = extractLocaleFromPath(pathname, config);
|
|
42
|
+
if (locale) return locale;
|
|
43
|
+
}
|
|
44
|
+
return config.defaultLocale;
|
|
45
|
+
}
|
|
46
|
+
function resolveLocaleAndConfig(override) {
|
|
47
|
+
const ctx = getLocaleContext();
|
|
48
|
+
const baseConfig = ctx?.config ?? DEFAULT_I18N_CONFIG;
|
|
49
|
+
const baseLocale = ctx?.locale ?? baseConfig.defaultLocale;
|
|
50
|
+
if (override === void 0) {
|
|
51
|
+
return { locale: baseLocale, config: baseConfig };
|
|
52
|
+
}
|
|
53
|
+
if (typeof override === "string") {
|
|
54
|
+
return { locale: override, config: baseConfig };
|
|
55
|
+
}
|
|
56
|
+
if ("locale" in override || "config" in override) {
|
|
57
|
+
const config2 = override.config ?? baseConfig;
|
|
58
|
+
const locale = override.locale ?? ctx?.locale ?? config2.defaultLocale;
|
|
59
|
+
return { locale, config: config2 };
|
|
60
|
+
}
|
|
61
|
+
const config = baseConfig;
|
|
62
|
+
return { locale: localeFromAstro(override, config), config };
|
|
63
|
+
}
|
|
64
|
+
function i18n(value, override) {
|
|
65
|
+
if (!isI18nValue(value)) return value;
|
|
66
|
+
const { locale, config } = resolveLocaleAndConfig(override);
|
|
67
|
+
return resolveI18nValue(value, locale, config);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// lib/runtime/middleware.ts
|
|
71
|
+
function deriveLocale(context, config) {
|
|
72
|
+
if (context?.currentLocale) return context.currentLocale;
|
|
73
|
+
const fromAstro = localeFromAstro(context, config);
|
|
74
|
+
return fromAstro || config.defaultLocale;
|
|
75
|
+
}
|
|
76
|
+
function createLocaleMiddleware(config) {
|
|
77
|
+
return (context, next) => {
|
|
78
|
+
const locale = deriveLocale(context ?? {}, config);
|
|
79
|
+
return runWithLocale(locale, config, () => next());
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
runWithLocale,
|
|
85
|
+
getLocaleContext,
|
|
86
|
+
localeFromAstro,
|
|
87
|
+
i18n,
|
|
88
|
+
deriveLocale,
|
|
89
|
+
createLocaleMiddleware
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=chunk-PQQAY42O.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../lib/runtime/i18n.ts", "../../lib/runtime/middleware.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * meno-astro \u2014 i18n runtime resolver.\n *\n * Emitted `.astro` markup carries i18n values verbatim and wraps them in a single-arg\n * `i18n({ _i18n: true, en: \"About\", pl: \"O nas\" })` call (see the dialect spec \u00A79). This\n * module implements that `i18n()` resolver plus the locale-context seam the future\n * `BaseLayout`/middleware will drive per route/locale.\n *\n * It reuses meno-core's primitives wholesale (`isI18nValue`, `resolveI18nValue`,\n * `extractLocaleFromPath`, `DEFAULT_I18N_CONFIG`) \u2014 no resolution logic is reinvented\n * here. The only new machinery is the `AsyncLocalStorage`-backed locale context.\n */\n\nimport {\n isI18nValue,\n resolveI18nValue,\n extractLocaleFromPath,\n DEFAULT_I18N_CONFIG,\n} from 'meno-core/shared';\nimport type { I18nConfig, I18nValue } from 'meno-core/shared/types';\n\n/** The per-render locale context: the active locale + the project's i18n config. */\nexport interface LocaleContextValue {\n locale: string;\n config: I18nConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Locale context store.\n//\n// SSR is concurrent: two requests for different locales can be in flight at once,\n// so a plain module variable would race. `AsyncLocalStorage` (node:async_hooks,\n// supported by Bun) scopes the context to the async call tree of `runWithLocale`.\n//\n// If `node:async_hooks` is somehow unavailable, we degrade to a documented\n// module-level variable rather than throwing at import time. That fallback is NOT\n// concurrency-safe (last-writer-wins across overlapping renders), but it keeps the\n// single-render / no-context paths working everywhere.\n// ---------------------------------------------------------------------------\n\ntype ALS = {\n getStore(): LocaleContextValue | undefined;\n run<R>(store: LocaleContextValue, fn: () => R): R;\n};\n\nlet store: ALS | undefined;\n/** Fallback holder used only when AsyncLocalStorage is unavailable. */\nlet fallbackContext: LocaleContextValue | undefined;\n\ntry {\n // Dynamic-ish require so a missing async_hooks degrades instead of crashing import.\n // (Static import would make this module fail to load on a runtime without it.)\n const { AsyncLocalStorage } = require('node:async_hooks') as {\n AsyncLocalStorage: new () => ALS;\n };\n store = new AsyncLocalStorage();\n} catch {\n store = undefined;\n}\n\n/**\n * Run `fn` with `{ locale, config }` as the active locale context. This is the seam the\n * future `BaseLayout`/middleware calls once per route/locale; everything rendered inside\n * (and `i18n()` calls within it) sees this locale. Returns `fn`'s return value.\n */\nexport function runWithLocale<T>(\n locale: string,\n config: I18nConfig,\n fn: () => T,\n): T {\n const ctx: LocaleContextValue = { locale, config };\n if (store) {\n return store.run(ctx, fn);\n }\n // Fallback path: set/restore the module-level variable around the call.\n const prev = fallbackContext;\n fallbackContext = ctx;\n try {\n return fn();\n } finally {\n fallbackContext = prev;\n }\n}\n\n/** Read the current locale context, or `undefined` if none is active. */\nexport function getLocaleContext(): LocaleContextValue | undefined {\n if (store) return store.getStore();\n return fallbackContext;\n}\n\n// ---------------------------------------------------------------------------\n// Astro-global \u2192 locale derivation.\n// ---------------------------------------------------------------------------\n\n/** The slice of the Astro global this module reads. */\nexport interface AstroLike {\n currentLocale?: string | null;\n url?: { pathname?: string } | null;\n}\n\n/**\n * Derive the active locale from an Astro global:\n * 1. `astro.currentLocale` (Astro's own i18n routing answer), else\n * 2. the locale prefix on `astro.url.pathname` (e.g. `/pl/about` \u2192 `pl`), else\n * 3. `config.defaultLocale`.\n */\nexport function localeFromAstro(astro: AstroLike | null | undefined, config: I18nConfig): string {\n if (astro?.currentLocale) return astro.currentLocale;\n const pathname = astro?.url?.pathname;\n if (typeof pathname === 'string') {\n const { locale } = extractLocaleFromPath(pathname, config);\n if (locale) return locale;\n }\n return config.defaultLocale;\n}\n\n// ---------------------------------------------------------------------------\n// i18n() \u2014 the emitter-facing resolver.\n// ---------------------------------------------------------------------------\n\n/**\n * Accepted second argument to `i18n()`. The single-arg call shape stays primary; this\n * override only narrows the locale/config when a caller already has them:\n * - a bare locale string (`\"pl\"`),\n * - `{ locale?, config? }`,\n * - an Astro-like `{ currentLocale?, url? }` (normalized via `localeFromAstro`).\n */\nexport type I18nOverride =\n | string\n | { locale?: string; config?: I18nConfig }\n | AstroLike;\n\n/** Resolve `override` (if any) to a concrete `{ locale, config }`, falling back to context/defaults. */\nfunction resolveLocaleAndConfig(override?: I18nOverride): LocaleContextValue {\n const ctx = getLocaleContext();\n const baseConfig = ctx?.config ?? DEFAULT_I18N_CONFIG;\n const baseLocale = ctx?.locale ?? baseConfig.defaultLocale;\n\n if (override === undefined) {\n return { locale: baseLocale, config: baseConfig };\n }\n\n if (typeof override === 'string') {\n return { locale: override, config: baseConfig };\n }\n\n // Plain { locale?, config? } override.\n if ('locale' in override || 'config' in override) {\n const config = override.config ?? baseConfig;\n const locale = override.locale ?? ctx?.locale ?? config.defaultLocale;\n return { locale, config };\n }\n\n // Astro-like override ({ currentLocale?, url? }).\n const config = baseConfig;\n return { locale: localeFromAstro(override as AstroLike, config), config };\n}\n\n/**\n * Resolve an i18n value to the active locale's string (or array, for list props).\n *\n * - Non-i18n values pass through unchanged (so the emitter can wrap any prop/attr/href\n * value uniformly).\n * - i18n values resolve via meno-core's `resolveI18nValue` fallback chain\n * (exact locale \u2192 defaultLocale \u2192 first available \u2192 empty string/array).\n * - Locale/config come from `override` if given, else the active `runWithLocale` context,\n * else `DEFAULT_I18N_CONFIG`'s default locale \u2014 a no-context call never throws.\n *\n * Single-arg `i18n(value)` is the primary call shape; `override` is optional.\n */\nexport function i18n<V>(value: V, override?: I18nOverride): V extends I18nValue ? string | unknown[] : V;\nexport function i18n(value: unknown, override?: I18nOverride): unknown {\n if (!isI18nValue(value)) return value;\n const { locale, config } = resolveLocaleAndConfig(override);\n return resolveI18nValue(value, locale, config);\n}\n", "/**\n * meno-astro \u2014 locale middleware factory.\n *\n * \u2500\u2500 Why middleware (and not BaseLayout) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * In Astro, the `i18n(...)` calls in a *page body* execute in the page component's\n * render scope, NOT inside `BaseLayout`'s frontmatter \u2014 so BaseLayout cannot wrap them\n * in `runWithLocale`. Astro **middleware**, by contrast, wraps the whole page render per\n * request (SSR) / per prerender (static): `onRequest(context, next)` runs `next()` to\n * render the matched route, and anything we establish around that `next()` call is in\n * scope for the entire page + all its components.\n *\n * So the locale seam is: `onRequest = (ctx, next) =>\n * runWithLocale(deriveLocale(ctx, config), config, () => next())`. The\n * `AsyncLocalStorage` context that `runWithLocale` opens is then read by every `i18n()`\n * call anywhere in that render tree (see `runtime/i18n.ts`).\n *\n * This module exports a **pure, testable factory** `createLocaleMiddleware(config)` plus\n * the `deriveLocale` policy it uses. The actual module Astro injects\n * (`runtime/localeMiddleware.ts`) wraps this factory, loading the project config once.\n *\n * \u2500\u2500 Typing without the `astro` dependency \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * `meno-astro` does not (yet) depend on `astro`, and no Astro toolchain is wired into\n * this package's test/type-check. Rather than import `MiddlewareHandler` /\n * `APIContext` from `astro` (which would not resolve here), we type against a **minimal\n * structural slice** of Astro's documented middleware contract:\n * - `context.currentLocale?: string` \u2014 Astro's own i18n-routing answer.\n * - `context.url?: URL` \u2014 the request URL (for path-prefix fallback).\n * - `next(): Promise<Response> | Response` \u2014 render the matched route.\n * Astro's real `MiddlewareHandler` is assignable to this shape, so the injected module\n * (`onRequest`) is correct-by-design against the documented API. The unit tests drive a\n * mock context with exactly this surface.\n */\n\nimport { runWithLocale, localeFromAstro } from './i18n';\nimport type { I18nConfig } from 'meno-core/shared/types';\n\n/**\n * The minimal slice of Astro's middleware `APIContext` this middleware reads.\n * Astro's real `APIContext` is structurally assignable to this.\n */\nexport interface LocaleMiddlewareContext {\n /** Astro's resolved locale for the current route (from its native i18n routing). */\n currentLocale?: string | null;\n /** The request URL \u2014 used for the `/pl/\u2026` path-prefix fallback. */\n url?: URL | { pathname?: string } | null;\n}\n\n/**\n * The minimal slice of Astro's `MiddlewareHandler` signature this factory returns.\n * `next` renders the matched route and yields its `Response`; the return value of the\n * handler is that same `Response`. Astro's real `MiddlewareHandler` is assignable here.\n */\nexport type LocaleMiddleware = (\n context: LocaleMiddlewareContext,\n next: () => Promise<Response> | Response,\n) => Promise<Response> | Response;\n\n/**\n * Derive the active locale for a request from its Astro context, against `config`:\n * 1. `context.currentLocale` \u2014 trust Astro's native i18n routing when it has an answer,\n * 2. else `localeFromAstro(context, config)` \u2014 the `/<locale>/\u2026` URL path-prefix\n * (which itself falls back to `config.defaultLocale`),\n * 3. else `config.defaultLocale` \u2014 a final guard (e.g. a falsy `currentLocale` AND a\n * missing/blank URL).\n *\n * Exported for direct unit testing of the policy in isolation.\n */\nexport function deriveLocale(context: LocaleMiddlewareContext, config: I18nConfig): string {\n if (context?.currentLocale) return context.currentLocale;\n // localeFromAstro reads `{ currentLocale, url: { pathname } }`; our context matches.\n const fromAstro = localeFromAstro(context as { currentLocale?: string | null; url?: { pathname?: string } | null }, config);\n return fromAstro || config.defaultLocale;\n}\n\n/**\n * Build a locale middleware bound to a concrete {@link I18nConfig}. The returned handler\n * derives the request's locale (via {@link deriveLocale}) and runs the rest of the render\n * (`next`) inside `runWithLocale(locale, config, \u2026)`, so every `i18n()` call in the page +\n * its components resolves to that locale.\n *\n * Pure and synchronous to construct (no filesystem / no Astro coupling), so a test can\n * call it with a mock `context` + mock `next` and assert end-to-end resolution.\n */\nexport function createLocaleMiddleware(config: I18nConfig): LocaleMiddleware {\n return (context, next) => {\n const locale = deriveLocale(context ?? {}, config);\n return runWithLocale(locale, config, () => next());\n };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;AA6CA,IAAI;AAEJ,IAAI;AAEJ,IAAI;AAGF,QAAM,EAAE,kBAAkB,IAAI,UAAQ,kBAAkB;AAGxD,UAAQ,IAAI,kBAAkB;AAChC,QAAQ;AACN,UAAQ;AACV;AAOO,SAAS,cACd,QACA,QACA,IACG;AACH,QAAM,MAA0B,EAAE,QAAQ,OAAO;AACjD,MAAI,OAAO;AACT,WAAO,MAAM,IAAI,KAAK,EAAE;AAAA,EAC1B;AAEA,QAAM,OAAO;AACb,oBAAkB;AAClB,MAAI;AACF,WAAO,GAAG;AAAA,EACZ,UAAE;AACA,sBAAkB;AAAA,EACpB;AACF;AAGO,SAAS,mBAAmD;AACjE,MAAI,MAAO,QAAO,MAAM,SAAS;AACjC,SAAO;AACT;AAkBO,SAAS,gBAAgB,OAAqC,QAA4B;AAC/F,MAAI,OAAO,cAAe,QAAO,MAAM;AACvC,QAAM,WAAW,OAAO,KAAK;AAC7B,MAAI,OAAO,aAAa,UAAU;AAChC,UAAM,EAAE,OAAO,IAAI,sBAAsB,UAAU,MAAM;AACzD,QAAI,OAAQ,QAAO;AAAA,EACrB;AACA,SAAO,OAAO;AAChB;AAmBA,SAAS,uBAAuB,UAA6C;AAC3E,QAAM,MAAM,iBAAiB;AAC7B,QAAM,aAAa,KAAK,UAAU;AAClC,QAAM,aAAa,KAAK,UAAU,WAAW;AAE7C,MAAI,aAAa,QAAW;AAC1B,WAAO,EAAE,QAAQ,YAAY,QAAQ,WAAW;AAAA,EAClD;AAEA,MAAI,OAAO,aAAa,UAAU;AAChC,WAAO,EAAE,QAAQ,UAAU,QAAQ,WAAW;AAAA,EAChD;AAGA,MAAI,YAAY,YAAY,YAAY,UAAU;AAChD,UAAMA,UAAS,SAAS,UAAU;AAClC,UAAM,SAAS,SAAS,UAAU,KAAK,UAAUA,QAAO;AACxD,WAAO,EAAE,QAAQ,QAAAA,QAAO;AAAA,EAC1B;AAGA,QAAM,SAAS;AACf,SAAO,EAAE,QAAQ,gBAAgB,UAAuB,MAAM,GAAG,OAAO;AAC1E;AAeO,SAAS,KAAK,OAAgB,UAAkC;AACrE,MAAI,CAAC,YAAY,KAAK,EAAG,QAAO;AAChC,QAAM,EAAE,QAAQ,OAAO,IAAI,uBAAuB,QAAQ;AAC1D,SAAO,iBAAiB,OAAO,QAAQ,MAAM;AAC/C;;;AC5GO,SAAS,aAAa,SAAkC,QAA4B;AACzF,MAAI,SAAS,cAAe,QAAO,QAAQ;AAE3C,QAAM,YAAY,gBAAgB,SAAkF,MAAM;AAC1H,SAAO,aAAa,OAAO;AAC7B;AAWO,SAAS,uBAAuB,QAAsC;AAC3E,SAAO,CAAC,SAAS,SAAS;AACxB,UAAM,SAAS,aAAa,WAAW,CAAC,GAAG,MAAM;AACjD,WAAO,cAAc,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAAA,EACnD;AACF;",
|
|
6
|
+
"names": ["config"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
__require,
|
|
14
|
+
__esm
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=chunk-ZYQNHI3W.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* BaseLayout.astro — the page shell every emitted page wraps its body in
|
|
4
|
+
* (`<BaseLayout meta={meta}>…</BaseLayout>`; see dialect/emit/emitPage.ts).
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - `<html lang>` from Astro's resolved locale (the locale middleware also drives this
|
|
8
|
+
* via Astro's native i18n routing); falls back to the runtime default locale.
|
|
9
|
+
* - `<head>` renders `meta.title` / `meta.description`, each **resolved through
|
|
10
|
+
* `i18n()`** because they may be i18n values (`{ _i18n, en, pl }`). The per-render
|
|
11
|
+
* locale context is already open here (the locale middleware wrapped this whole
|
|
12
|
+
* render in `runWithLocale`), so `i18n()` resolves to the active locale.
|
|
13
|
+
* - `<body><slot /></body>` — the page body.
|
|
14
|
+
* - After the slot, drains the `style()` CSS collector into a `<style>` tag so the CSS
|
|
15
|
+
* accumulated while rendering the page's nodes lands in the document.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: the page body's own `i18n(...)` calls run in the page's render scope, not here —
|
|
18
|
+
* which is exactly why the *middleware* (not this layout) establishes the locale context.
|
|
19
|
+
* This layout only resolves the `meta` values it is handed.
|
|
20
|
+
*/
|
|
21
|
+
import { i18n, flushCollectedStyles } from 'meno-astro';
|
|
22
|
+
import { DEFAULT_I18N_CONFIG } from 'meno-core/shared/i18n';
|
|
23
|
+
import type { MenoPageMeta } from 'meno-astro';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
meta?: MenoPageMeta;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { meta } = Astro.props;
|
|
30
|
+
|
|
31
|
+
// Active locale: Astro's i18n routing answer, else the runtime default. Used for <html lang>.
|
|
32
|
+
const lang = Astro.currentLocale ?? DEFAULT_I18N_CONFIG.defaultLocale;
|
|
33
|
+
|
|
34
|
+
// meta.title / meta.description may be plain strings OR i18n values — resolve both through
|
|
35
|
+
// i18n() (a non-i18n value passes through unchanged). The middleware-opened locale context
|
|
36
|
+
// makes these resolve to the active locale.
|
|
37
|
+
const title = i18n(meta?.title ?? '');
|
|
38
|
+
const description = i18n(meta?.description ?? '');
|
|
39
|
+
---
|
|
40
|
+
<!doctype html>
|
|
41
|
+
<html lang={lang}>
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
45
|
+
{title ? <title>{title}</title> : null}
|
|
46
|
+
{description ? <meta name="description" content={String(description)} /> : null}
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<slot />
|
|
50
|
+
{/* Drain the style() collector (CSS accumulated while rendering the body) into the page. */}
|
|
51
|
+
<style set:html={flushCollectedStyles()}></style>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Embed.astro — the runtime component emitted for `{ type: "embed" }` nodes
|
|
4
|
+
* (see dialect/emit/emitNode.ts:renderEmbed). The emitter passes `html` (a raw HTML
|
|
5
|
+
* string), an optional `class`, and any other attributes.
|
|
6
|
+
*
|
|
7
|
+
* Minimal but correct: inject the raw `html` verbatim. When a wrapper is needed (a
|
|
8
|
+
* `class` or extra attributes were supplied) we wrap in a `<div>` carrying them;
|
|
9
|
+
* otherwise we emit the HTML bare via a `<Fragment>` so no extra element is introduced.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
html?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
[attr: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { html = '', class: className, ...rest } = Astro.props;
|
|
18
|
+
const hasWrapperAttrs = Boolean(className) || Object.keys(rest).length > 0;
|
|
19
|
+
---
|
|
20
|
+
{hasWrapperAttrs ? (
|
|
21
|
+
<div class={className} {...rest} set:html={html} />
|
|
22
|
+
) : (
|
|
23
|
+
<Fragment set:html={html} />
|
|
24
|
+
)}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Link.astro — the runtime component emitted for `{ type: "link" }` nodes
|
|
4
|
+
* (see dialect/emit/emitNode.ts:renderLink). The emitter passes `href` (already a
|
|
5
|
+
* resolved string — via a `{{…}}` template, an `i18n(...)`, or an `href(...)` call), an
|
|
6
|
+
* optional `class`, plus any other HTML attributes, and the link text/children as a slot.
|
|
7
|
+
*
|
|
8
|
+
* Minimal but correct: render an `<a>` with `href` + `class`, spread the remaining
|
|
9
|
+
* attributes verbatim, and project children through `<slot />`.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
href?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
[attr: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { href, class: className, ...rest } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
<a href={href} class={className} {...rest}><slot /></a>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LocaleList.astro — the locale switcher (`{ type: "locale-list" }` nodes;
|
|
4
|
+
* see dialect/emit/emitNode.ts:renderLocaleList).
|
|
5
|
+
*
|
|
6
|
+
* Renders one link per configured locale, pointing at the *current page* in that locale
|
|
7
|
+
* (computed with meno-core's `buildLocalizedPath` over the path with its current locale
|
|
8
|
+
* prefix stripped). The link for the active locale (`Astro.currentLocale`) is marked
|
|
9
|
+
* `aria-current="true"` and given an `is-active` class so the page can style it.
|
|
10
|
+
*
|
|
11
|
+
* The locale set comes from the project's i18n config (`loadI18nConfig(process.cwd())` —
|
|
12
|
+
* the same config the middleware/integration use), so this stays in sync with Astro's
|
|
13
|
+
* configured locales. Display label uses each locale's `nativeName` (its public-facing
|
|
14
|
+
* name, e.g. "Polski"), falling back to the code.
|
|
15
|
+
*
|
|
16
|
+
* Props mirror the emitter: `class`, `showCurrent` (include the active locale's link;
|
|
17
|
+
* default true), `displayType`, and pre-resolved `style`/`itemStyle`/`activeItemStyle`
|
|
18
|
+
* class strings. They are applied where present; unknown extras are ignored.
|
|
19
|
+
*/
|
|
20
|
+
import { loadI18nConfig } from 'meno-astro/server';
|
|
21
|
+
import { buildLocalizedPath, extractLocaleFromPath } from 'meno-core/shared/i18n';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
class?: string;
|
|
25
|
+
showCurrent?: boolean;
|
|
26
|
+
displayType?: string;
|
|
27
|
+
style?: string;
|
|
28
|
+
itemStyle?: string;
|
|
29
|
+
activeItemStyle?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
class: className = '',
|
|
34
|
+
showCurrent = true,
|
|
35
|
+
style = '',
|
|
36
|
+
itemStyle = '',
|
|
37
|
+
activeItemStyle = '',
|
|
38
|
+
} = Astro.props;
|
|
39
|
+
|
|
40
|
+
const config = loadI18nConfig(process.cwd());
|
|
41
|
+
const current = Astro.currentLocale ?? config.defaultLocale;
|
|
42
|
+
|
|
43
|
+
// Strip any existing locale prefix so we can re-prefix per target locale.
|
|
44
|
+
const pathname = Astro.url?.pathname ?? '/';
|
|
45
|
+
const { pathWithoutLocale } = extractLocaleFromPath(pathname, config);
|
|
46
|
+
|
|
47
|
+
const items = config.locales
|
|
48
|
+
.filter((loc) => showCurrent || loc.code !== current)
|
|
49
|
+
.map((loc) => {
|
|
50
|
+
const isActive = loc.code === current;
|
|
51
|
+
// Default locale lives un-prefixed (matches integration's prefixDefaultLocale: false).
|
|
52
|
+
const href =
|
|
53
|
+
loc.code === config.defaultLocale
|
|
54
|
+
? pathWithoutLocale || '/'
|
|
55
|
+
: buildLocalizedPath(pathWithoutLocale, loc.code);
|
|
56
|
+
return { loc, isActive, href, label: loc.nativeName || loc.code };
|
|
57
|
+
});
|
|
58
|
+
---
|
|
59
|
+
<ul class={[style, className].filter(Boolean).join(' ')} data-locale-list>
|
|
60
|
+
{items.map(({ loc, isActive, href, label }) => (
|
|
61
|
+
<li>
|
|
62
|
+
<a
|
|
63
|
+
href={href}
|
|
64
|
+
hreflang={loc.langTag}
|
|
65
|
+
lang={loc.langTag}
|
|
66
|
+
aria-current={isActive ? 'true' : undefined}
|
|
67
|
+
class={[itemStyle, isActive ? activeItemStyle : '', isActive ? 'is-active' : '']
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(' ')}
|
|
70
|
+
>
|
|
71
|
+
{label}
|
|
72
|
+
</a>
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "../../chunks/chunk-ZYQNHI3W.js";
|
|
2
|
+
|
|
3
|
+
// lib/components/index.ts
|
|
4
|
+
import { default as default2 } from "./BaseLayout.astro";
|
|
5
|
+
import { default as default3 } from "./Link.astro";
|
|
6
|
+
import { default as default4 } from "./Embed.astro";
|
|
7
|
+
import { default as default5 } from "./LocaleList.astro";
|
|
8
|
+
export {
|
|
9
|
+
default2 as BaseLayout,
|
|
10
|
+
default4 as Embed,
|
|
11
|
+
default3 as Link,
|
|
12
|
+
default5 as LocaleList
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../lib/components/index.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * meno-astro/components \u2014 the runtime `.astro` components emitted pages/components import.\n *\n * Emitted markup uses `import { BaseLayout, Link, Embed, LocaleList } from\n * 'meno-astro/components'` (see dialect/emit/frontmatter.ts component-import lines). This\n * barrel re-exports each `.astro` file as a named export so those imports resolve.\n *\n * NOTE: these are `.astro` modules \u2014 they are compiled by Astro's Vite plugin at build\n * time, not by `tsc`. This barrel is the resolution surface; type-checking of the\n * components themselves happens under `astro check`, not this package's `tsc` run.\n */\n\nexport { default as BaseLayout } from './BaseLayout.astro';\nexport { default as Link } from './Link.astro';\nexport { default as Embed } from './Embed.astro';\nexport { default as LocaleList } from './LocaleList.astro';\n"],
|
|
5
|
+
"mappings": ";;;AAYA,SAAoB,WAAXA,gBAA6B;AACtC,SAAoB,WAAXA,gBAAuB;AAChC,SAAoB,WAAXA,gBAAwB;AACjC,SAAoB,WAAXA,gBAA6B;",
|
|
6
|
+
"names": ["default"]
|
|
7
|
+
}
|