@texuli/astro 0.0.0-stage → 0.1.0-next.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/LICENSE +21 -0
- package/README.md +100 -2
- package/dist/content/index.d.ts +11 -0
- package/dist/content/index.js +8 -0
- package/dist/css-modules.d.ts +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/integration/index.d.ts +25 -0
- package/dist/integration/index.js +72 -0
- package/dist/layouts/ContentLayout.astro +68 -0
- package/dist/layouts/ContentLayout.d.ts +3 -0
- package/dist/layouts/ContentLayout.js +4 -0
- package/dist/sections/Hero.astro +34 -0
- package/dist/sections/Hero.d.ts +3 -0
- package/dist/sections/Hero.js +4 -0
- package/dist/sections/Section.astro +42 -0
- package/dist/sections/Section.d.ts +3 -0
- package/dist/sections/Section.js +4 -0
- package/dist/site/index.d.ts +86 -0
- package/dist/site/index.js +182 -0
- package/dist/styles.css +36 -0
- package/dist/styles.css.d.ts +1 -0
- package/package.json +38 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dailybot, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @texuli/astro
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Astro 7 integration, token stylesheet, static content components, and a closed
|
|
4
|
+
content-block parser for Texuli sites. The package is private and source-first
|
|
5
|
+
while Texuli is in development; it is not published to npm.
|
|
6
|
+
|
|
7
|
+
## Install and configure
|
|
8
|
+
|
|
9
|
+
Add the Astro Svelte integration when the site uses Svelte islands, then add
|
|
10
|
+
Texuli's integration to the Astro config:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import svelte from '@astrojs/svelte';
|
|
14
|
+
import { defineConfig } from 'astro/config';
|
|
15
|
+
import { texuli } from '@texuli/astro';
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
integrations: [svelte(), texuli({ namespace: 'docs' })],
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Import the Texuli stylesheet once from a site-owned global stylesheet:
|
|
23
|
+
|
|
24
|
+
```css
|
|
25
|
+
@import '@texuli/astro/styles.css';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use the same `namespace` in Texuli's appearance writer and the integration so
|
|
29
|
+
the early bootstrap reads the cache entry that the site writes. The default
|
|
30
|
+
namespace is `site`. See [integration setup and CSP](docs/integration.md) and
|
|
31
|
+
[stylesheet contents](docs/styles.md).
|
|
32
|
+
|
|
33
|
+
## Entry points
|
|
34
|
+
|
|
35
|
+
| Import | Purpose | Documentation |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `@texuli/astro` | `texuli`, `themeBootstrapScript`, `TexuliAstroOptions`, density/radius types, and `parseTexuliBlocks` | [Integration](docs/integration.md), [content parser](docs/content.md) |
|
|
38
|
+
| `@texuli/astro/integration` | Integration and theme-bootstrap API | [Integration](docs/integration.md) |
|
|
39
|
+
| `@texuli/astro/content` | Closed-schema content-block parser | [Content parser](docs/content.md) |
|
|
40
|
+
| `@texuli/astro/site` | Validated site model, page metadata and sitemap for Protoyard static sites | [Site model](docs/site.md) |
|
|
41
|
+
| `@texuli/astro/styles.css` | Token base, forced-colors rules, density/radius variants, and print rules | [Stylesheet](docs/styles.md) |
|
|
42
|
+
| `@texuli/astro/layouts/ContentLayout` | Semantic HTML document shell | [ContentLayout](docs/layouts-content-layout.md) |
|
|
43
|
+
| `@texuli/astro/sections/Hero` | Static hero section | [Hero](docs/sections-hero.md) |
|
|
44
|
+
| `@texuli/astro/sections/Section` | Static width-constrained section | [Section](docs/sections-section.md) |
|
|
45
|
+
|
|
46
|
+
The package peers on Astro Svelte integration and Svelte 5 for sites that use
|
|
47
|
+
Svelte islands. The layout and section components in this package render static
|
|
48
|
+
HTML and do not hydrate themselves.
|
|
49
|
+
|
|
50
|
+
## Components and content
|
|
51
|
+
|
|
52
|
+
```astro
|
|
53
|
+
---
|
|
54
|
+
import '@texuli/astro/styles.css';
|
|
55
|
+
import ContentLayout from '@texuli/astro/layouts/ContentLayout';
|
|
56
|
+
import Hero from '@texuli/astro/sections/Hero';
|
|
57
|
+
import Section from '@texuli/astro/sections/Section';
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<ContentLayout title="Stories" description="A collection of stories.">
|
|
61
|
+
<Hero title="Stories" align="center">
|
|
62
|
+
<a href="https://example.com/stories">Browse stories</a>
|
|
63
|
+
</Hero>
|
|
64
|
+
<Section width="reading">
|
|
65
|
+
<p>Static Astro content.</p>
|
|
66
|
+
</Section>
|
|
67
|
+
</ContentLayout>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For structured content, `parseTexuliBlocks(value)` validates unknown input
|
|
71
|
+
against `@texuli/blocks` and returns either the typed blocks or schema issues.
|
|
72
|
+
It does not render HTML or interpret unknown block types. See [content
|
|
73
|
+
parsing](docs/content.md). For whole-site definitions — pages, navigation,
|
|
74
|
+
canonical metadata and a sitemap — `defineSite` validates the export and
|
|
75
|
+
returns the frozen model the layouts consume. See the [site
|
|
76
|
+
model](docs/site.md). The Protoyard `site` export that fills this model is
|
|
77
|
+
planned on the Protoyard side.
|
|
78
|
+
|
|
79
|
+
## Fixtures and checks
|
|
80
|
+
|
|
81
|
+
The packed consumer fixtures live under `tests/consumers/fixtures/`:
|
|
82
|
+
|
|
83
|
+
- `astro-plain` installs the packed Astro package, uses this integration and
|
|
84
|
+
stylesheet, renders the layout and sections, and includes a Svelte smoke
|
|
85
|
+
island.
|
|
86
|
+
- `astro-tailwind` checks the Astro/Tailwind consumer path with token CSS; it
|
|
87
|
+
does not install `@texuli/astro`.
|
|
88
|
+
|
|
89
|
+
Package behavior is covered by `packages/astro/tests/content.test.ts`,
|
|
90
|
+
`packages/astro/tests/integration.test.ts`, and
|
|
91
|
+
`packages/astro/tests/site.test.ts`. The consumer harness builds and
|
|
92
|
+
serves the packed fixtures in `tests/consumers/consumers.spec.ts`.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pnpm vitest run packages/astro
|
|
96
|
+
pnpm --filter @texuli/astro typecheck
|
|
97
|
+
npx biome check packages/astro
|
|
98
|
+
pnpm verify:packages --filter packages/astro
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The package source and exports are listed in [`package.json`](package.json).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import { type Block, type FieldIssue } from '@texuli/blocks/core';
|
|
3
|
+
export type ParsedTexuliBlocks = {
|
|
4
|
+
success: true;
|
|
5
|
+
blocks: Block[];
|
|
6
|
+
} | {
|
|
7
|
+
success: false;
|
|
8
|
+
issues: FieldIssue[];
|
|
9
|
+
};
|
|
10
|
+
/** Parse a content entry's block field using the closed Texuli block schema. */
|
|
11
|
+
export declare function parseTexuliBlocks(value: unknown): ParsedTexuliBlocks;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { safeParseBlocks } from '@texuli/blocks/core';
|
|
2
|
+
/** Parse a content entry's block field using the closed Texuli block schema. */
|
|
3
|
+
export function parseTexuliBlocks(value) {
|
|
4
|
+
const parsed = safeParseBlocks(value);
|
|
5
|
+
if (!parsed.success)
|
|
6
|
+
return { success: false, issues: parsed.issues };
|
|
7
|
+
return { success: true, blocks: parsed.data };
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.css';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/// <reference path="./css-modules.d.ts" />
|
|
2
|
+
export { type ParsedTexuliBlocks, parseTexuliBlocks } from './content/index.js';
|
|
3
|
+
export { type TexuliAstroOptions, type TexuliDensityVariant, type TexuliRadiusVariant, texuli, themeBootstrapScript, } from './integration/index.js';
|
|
4
|
+
export { defineSite, SITE_KIND, sitePage, sitePageMeta, siteSitemap, type TexuliSite, type TexuliSiteInput, type TexuliSiteNavEntry, type TexuliSitePage, type TexuliSitePageInput, } from './site/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import type { ColorMode } from '@texuli/tokens/modes';
|
|
3
|
+
import type { AstroIntegration } from 'astro';
|
|
4
|
+
export type TexuliDensityVariant = 'compact' | 'comfortable' | 'spacious';
|
|
5
|
+
export type TexuliRadiusVariant = 'compact' | 'comfortable' | 'rounded';
|
|
6
|
+
export interface TexuliAstroOptions {
|
|
7
|
+
/** Namespace shared with the appearance switcher that writes the token cache. */
|
|
8
|
+
namespace?: string;
|
|
9
|
+
/** Mode used without a cached appearance. Defaults to the active system preference. */
|
|
10
|
+
fallbackMode?: ColorMode;
|
|
11
|
+
/** Select a token density variant on the document root before styles paint. */
|
|
12
|
+
density?: TexuliDensityVariant;
|
|
13
|
+
/** Select a token radius scale on the document root before styles paint. */
|
|
14
|
+
radius?: TexuliRadiusVariant;
|
|
15
|
+
/** Use Astro's inline-script injection, or render the exported body with a host nonce. */
|
|
16
|
+
bootstrap?: 'injected' | 'manual';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Return the deterministic inline theme bootstrap body. It restores the cached
|
|
20
|
+
* appearance before paint, follows system preference changes when uncached,
|
|
21
|
+
* and applies same-origin storage updates without a reload or network request.
|
|
22
|
+
*/
|
|
23
|
+
export declare function themeBootstrapScript(options?: TexuliAstroOptions): string;
|
|
24
|
+
/** Install the deterministic, CSP-compatible early theme bootstrap for Astro. */
|
|
25
|
+
export declare function texuli(options?: TexuliAstroOptions): AstroIntegration;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { COLOR_MODE_NAMES } from '@texuli/tokens/modes';
|
|
2
|
+
import { appearanceCacheKey, noFlashScript } from '@texuli/tokens/no-flash';
|
|
3
|
+
const DENSITIES = ['compact', 'comfortable', 'spacious'];
|
|
4
|
+
const RADII = ['compact', 'comfortable', 'rounded'];
|
|
5
|
+
function optionJson(value) {
|
|
6
|
+
return JSON.stringify(value)
|
|
7
|
+
.replace(/</g, '\\u003c')
|
|
8
|
+
.replace(/>/g, '\\u003e')
|
|
9
|
+
.replace(/&/g, '\\u0026')
|
|
10
|
+
.replace(/\u2028/g, '\\u2028')
|
|
11
|
+
.replace(/\u2029/g, '\\u2029');
|
|
12
|
+
}
|
|
13
|
+
function validateOptions(options) {
|
|
14
|
+
const namespace = options.namespace ?? 'site';
|
|
15
|
+
const fallbackMode = options.fallbackMode ?? 'system';
|
|
16
|
+
const bootstrap = options.bootstrap ?? 'injected';
|
|
17
|
+
// Keep validation aligned with the token cache's namespaced contract.
|
|
18
|
+
appearanceCacheKey(namespace);
|
|
19
|
+
noFlashScript(namespace, fallbackMode);
|
|
20
|
+
if (!COLOR_MODE_NAMES.includes(fallbackMode)) {
|
|
21
|
+
throw new TypeError('Fallback mode must be a supported Texuli color mode.');
|
|
22
|
+
}
|
|
23
|
+
if (options.density !== undefined && !DENSITIES.includes(options.density)) {
|
|
24
|
+
throw new TypeError('Density must be compact, comfortable, or spacious.');
|
|
25
|
+
}
|
|
26
|
+
if (options.radius !== undefined && !RADII.includes(options.radius)) {
|
|
27
|
+
throw new TypeError('Radius must be compact, comfortable, or rounded.');
|
|
28
|
+
}
|
|
29
|
+
if (bootstrap !== 'injected' && bootstrap !== 'manual') {
|
|
30
|
+
throw new TypeError('Bootstrap must be injected or manual.');
|
|
31
|
+
}
|
|
32
|
+
return { namespace, fallbackMode, bootstrap, density: options.density, radius: options.radius };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Return the deterministic inline theme bootstrap body. It restores the cached
|
|
36
|
+
* appearance before paint, follows system preference changes when uncached,
|
|
37
|
+
* and applies same-origin storage updates without a reload or network request.
|
|
38
|
+
*/
|
|
39
|
+
export function themeBootstrapScript(options = {}) {
|
|
40
|
+
const { namespace, fallbackMode, density, radius } = validateOptions(options);
|
|
41
|
+
const key = optionJson(appearanceCacheKey(namespace));
|
|
42
|
+
const densityJson = density ? optionJson(density) : 'null';
|
|
43
|
+
const radiusJson = radius ? optionJson(radius) : 'null';
|
|
44
|
+
const initial = noFlashScript(namespace, fallbackMode);
|
|
45
|
+
const sync = `(function(){var d=document.documentElement,k=${key},f=${optionJson(fallbackMode)},dv=${densityJson},rv=${radiusJson},applied=[];` +
|
|
46
|
+
`function validValue(v){return typeof v==='string'&&v.length<=2048&&!/[;{}<>]/.test(v)&&!/[\\x00-\\x1f\\x7f]/.test(v)&&!/(?:url\\s*\\(|expression\\s*\\(|var\\s*\\(|javascript:|@import)/i.test(v);}` +
|
|
47
|
+
`function read(raw){try{var c=JSON.parse(raw||'null');if(!c||c.v!==1||typeof c.themeId!=='string'||!/^[a-z][a-z0-9-]{0,79}$/.test(c.themeId)||(c.mode!=='light'&&c.mode!=='dark'&&c.mode!=='high-contrast')||!c.vars||typeof c.vars!=='object'||Array.isArray(c.vars))return null;var vars={};for(var n in c.vars){if(Object.prototype.hasOwnProperty.call(c.vars,n)&&/^--tx-[a-z0-9-]+$/.test(n)&&validValue(c.vars[n]))vars[n]=c.vars[n];}return {themeId:c.themeId,mode:c.mode,vars:vars,reducedMotion:c.reducedMotion,surface:c.surface,components:c.components||{}};}catch(e){return null;}}` +
|
|
48
|
+
`function modeFromSystem(){try{if(matchMedia('(prefers-contrast: more)').matches)return 'high-contrast';return matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}catch(e){return 'light';}}` +
|
|
49
|
+
`function setMode(m){d.dataset.txMode=m;d.classList.toggle('dark',m==='dark');d.classList.toggle('light',m==='light');d.style.colorScheme=m==='high-contrast'?'light dark':m;}` +
|
|
50
|
+
`function setVariant(){if(dv)d.dataset.txDensity=dv;else delete d.dataset.txDensity;if(rv)d.dataset.txRadius=rv;else delete d.dataset.txRadius;}` +
|
|
51
|
+
`function apply(c){for(var i=0;i<applied.length;i++)d.style.removeProperty(applied[i]);applied=[];if(!c){delete d.dataset.txTheme;delete d.dataset.txMotion;delete d.dataset.txSurface;delete d.dataset.txButtonStyle;delete d.dataset.txCardStyle;delete d.dataset.txFocusRingStyle;setMode(f==='system'?modeFromSystem():f);return;}d.dataset.txTheme=c.themeId;for(var n in c.vars){var v=c.vars[n];if(dv&&n==='--tx-density')v=String(${density === 'compact' ? '0.875' : density === 'spacious' ? '1.125' : '1'});if(rv&&n==='--tx-radius-scale')v=String(${radius === 'compact' ? '0.75' : radius === 'rounded' ? '1.25' : '1'});if(rv&&(n==='--tx-radius'||/^--tx-radius-step-[0-9]+$/.test(n)))v='calc('+v+' * '+(${radius === 'compact' ? '0.75' : radius === 'rounded' ? '1.25' : '1'})+')';d.style.setProperty(n,v);applied.push(n);}if(dv&&!c.vars['--tx-density']){d.style.setProperty('--tx-density',${density === 'compact' ? "'0.875'" : density === 'spacious' ? "'1.125'" : "'1'"});applied.push('--tx-density');}if(rv&&!c.vars['--tx-radius-scale']){d.style.setProperty('--tx-radius-scale',${radius === 'compact' ? "'0.75'" : radius === 'rounded' ? "'1.25'" : "'1'"});applied.push('--tx-radius-scale');}if(c.reducedMotion===true||c.reducedMotion===false)d.dataset.txMotion=c.reducedMotion?'reduce':'full';else delete d.dataset.txMotion;if(c.surface==='glass'||c.surface==='solid')d.dataset.txSurface=c.surface;else delete d.dataset.txSurface;var p=c.components;if(p.button==='pill'||p.button==='square')d.dataset.txButtonStyle=p.button;else delete d.dataset.txButtonStyle;if(p.card==='glass'||p.card==='bordered')d.dataset.txCardStyle=p.card;else delete d.dataset.txCardStyle;if(p.focusRing==='ring'||p.focusRing==='outline'||p.focusRing==='glow'||p.focusRing==='inset')d.dataset.txFocusRingStyle=p.focusRing;else delete d.dataset.txFocusRingStyle;setMode(c.mode);}` +
|
|
52
|
+
`function current(){try{return read(localStorage.getItem(k));}catch(e){return null;}}` +
|
|
53
|
+
`function syncSystem(){if(f==='system'&&!current())setMode(modeFromSystem());}` +
|
|
54
|
+
`var boot=current();if(boot)for(var name in boot.vars)if(/^--tx-[a-z0-9-]+$/.test(name)&&validValue(boot.vars[name]))applied.push(name);setVariant();apply(boot);if(typeof addEventListener==='function')addEventListener('storage',function(e){if(e.key===k)apply(read(e.newValue));});` +
|
|
55
|
+
`if(f==='system'&&typeof matchMedia==='function'){var q1=matchMedia('(prefers-color-scheme: dark)'),q2=matchMedia('(prefers-contrast: more)');if(q1.addEventListener)q1.addEventListener('change',syncSystem);else if(q1.addListener)q1.addListener(syncSystem);if(q2.addEventListener)q2.addEventListener('change',syncSystem);else if(q2.addListener)q2.addListener(syncSystem);}` +
|
|
56
|
+
`})();`;
|
|
57
|
+
return `${initial}${sync}`;
|
|
58
|
+
}
|
|
59
|
+
/** Install the deterministic, CSP-compatible early theme bootstrap for Astro. */
|
|
60
|
+
export function texuli(options = {}) {
|
|
61
|
+
const normalized = validateOptions(options);
|
|
62
|
+
const script = themeBootstrapScript(normalized);
|
|
63
|
+
return {
|
|
64
|
+
name: '@texuli/astro',
|
|
65
|
+
hooks: {
|
|
66
|
+
'astro:config:setup': ({ injectScript }) => {
|
|
67
|
+
if (normalized.bootstrap === 'injected')
|
|
68
|
+
injectScript('head-inline', script);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { safeHref } from '@texuli/core/url-policy';
|
|
3
|
+
import type { TexuliDensityVariant, TexuliRadiusVariant } from '../integration/index.ts';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
lang?: string;
|
|
9
|
+
theme?: string;
|
|
10
|
+
canonical?: string;
|
|
11
|
+
/** Static token density variant; matches the integration's script-driven attribute. */
|
|
12
|
+
density?: TexuliDensityVariant;
|
|
13
|
+
/** Static token radius scale; matches the integration's script-driven attribute. */
|
|
14
|
+
radius?: TexuliRadiusVariant;
|
|
15
|
+
ogTitle?: string;
|
|
16
|
+
ogDescription?: string;
|
|
17
|
+
/** Open Graph image; rendered only when it passes the developer URL policy. */
|
|
18
|
+
ogImage?: string;
|
|
19
|
+
/** Asks crawlers to skip the page; use for drafts and staging mirrors. */
|
|
20
|
+
noindex?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DENSITIES: readonly TexuliDensityVariant[] = ['compact', 'comfortable', 'spacious'];
|
|
24
|
+
const RADII: readonly TexuliRadiusVariant[] = ['compact', 'comfortable', 'rounded'];
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
lang = 'en',
|
|
30
|
+
theme = 'default',
|
|
31
|
+
canonical,
|
|
32
|
+
density,
|
|
33
|
+
radius,
|
|
34
|
+
ogTitle,
|
|
35
|
+
ogDescription,
|
|
36
|
+
ogImage,
|
|
37
|
+
noindex = false,
|
|
38
|
+
} = Astro.props;
|
|
39
|
+
|
|
40
|
+
if (density !== undefined && !DENSITIES.includes(density)) {
|
|
41
|
+
throw new TypeError('Density must be compact, comfortable, or spacious.');
|
|
42
|
+
}
|
|
43
|
+
if (radius !== undefined && !RADII.includes(radius)) {
|
|
44
|
+
throw new TypeError('Radius must be compact, comfortable, or rounded.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const canonicalHref = safeHref(canonical, 'developer');
|
|
48
|
+
const ogImageHref = safeHref(ogImage, 'developer');
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
<!doctype html>
|
|
52
|
+
<html lang={lang} data-tx-theme={theme} data-tx-density={density} data-tx-radius={radius}>
|
|
53
|
+
<head>
|
|
54
|
+
<meta charset="utf-8" />
|
|
55
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
56
|
+
<title>{title}</title>
|
|
57
|
+
{description && <meta name="description" content={description} />}
|
|
58
|
+
{canonicalHref && <link rel="canonical" href={canonicalHref} />}
|
|
59
|
+
{ogTitle && <meta property="og:title" content={ogTitle} />}
|
|
60
|
+
{ogDescription && <meta property="og:description" content={ogDescription} />}
|
|
61
|
+
{ogImageHref && <meta property="og:image" content={ogImageHref} />}
|
|
62
|
+
{noindex && <meta name="robots" content="noindex" />}
|
|
63
|
+
<slot name="head" />
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<slot />
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
eyebrow?: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
align?: 'start' | 'center';
|
|
7
|
+
/** `lede` (default) for page tops; `compact` for sub-pages and nested views. */
|
|
8
|
+
size?: 'lede' | 'compact';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { eyebrow, title, description, align = 'start', size = 'lede' } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<header
|
|
15
|
+
class:list={['tx-astro-hero', `tx-astro-hero--${align}`, `tx-astro-hero--${size}`]}
|
|
16
|
+
>
|
|
17
|
+
{eyebrow && <p class="tx-astro-hero__eyebrow">{eyebrow}</p>}
|
|
18
|
+
<h1>{title}</h1>
|
|
19
|
+
{description && <p class="tx-astro-hero__description">{description}</p>}
|
|
20
|
+
<div class="tx-astro-hero__actions"><slot /></div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.tx-astro-hero { display: grid; gap: 1rem; padding-block: clamp(3rem, 9vw, 7rem); }
|
|
25
|
+
.tx-astro-hero--center { text-align: center; justify-items: center; }
|
|
26
|
+
.tx-astro-hero__eyebrow { color: var(--tx-color-accent-foreground, inherit); font-weight: 600; margin: 0; }
|
|
27
|
+
h1 { font-size: clamp(2.5rem, 7vw, 5rem); line-height: 1.05; max-inline-size: 16ch; margin: 0; }
|
|
28
|
+
.tx-astro-hero__description { color: var(--tx-color-muted-foreground, currentColor); font-size: 1.125rem; line-height: 1.6; max-inline-size: 62ch; margin: 0; }
|
|
29
|
+
.tx-astro-hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-block-start: 0.5rem; }
|
|
30
|
+
|
|
31
|
+
.tx-astro-hero--compact { gap: 0.75rem; padding-block: clamp(1.5rem, 5vw, 3rem); }
|
|
32
|
+
.tx-astro-hero--compact h1 { font-size: clamp(1.75rem, 4.5vw, 2.75rem); }
|
|
33
|
+
.tx-astro-hero--compact .tx-astro-hero__description { font-size: 1rem; }
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
width?: 'reading' | 'wide' | 'full';
|
|
6
|
+
/** `surface` paints the theme's surface color behind the section. */
|
|
7
|
+
tone?: 'default' | 'surface';
|
|
8
|
+
/** `none` removes the responsive inline padding, for nesting sections. */
|
|
9
|
+
padding?: 'normal' | 'none';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { id, label, width = 'reading', tone = 'default', padding = 'normal' } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<section
|
|
16
|
+
id={id}
|
|
17
|
+
aria-label={label}
|
|
18
|
+
class:list={[
|
|
19
|
+
'tx-astro-section',
|
|
20
|
+
`tx-astro-section--${width}`,
|
|
21
|
+
`tx-astro-section--${tone}`,
|
|
22
|
+
`tx-astro-section--padding-${padding}`,
|
|
23
|
+
]}
|
|
24
|
+
>
|
|
25
|
+
<slot />
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.tx-astro-section {
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
margin-inline: auto;
|
|
32
|
+
padding-inline: clamp(1rem, 4vw, 2rem);
|
|
33
|
+
inline-size: 100%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.tx-astro-section--reading { max-inline-size: 72rem; }
|
|
37
|
+
.tx-astro-section--wide { max-inline-size: 88rem; }
|
|
38
|
+
.tx-astro-section--full { max-inline-size: none; }
|
|
39
|
+
|
|
40
|
+
.tx-astro-section--surface { background: var(--tx-surface); }
|
|
41
|
+
.tx-astro-section--padding-none { padding-inline: 0; }
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import { type Block } from '@texuli/blocks/core';
|
|
3
|
+
/**
|
|
4
|
+
* The site kind identifier that Protoyard static-site exports declare. A
|
|
5
|
+
* `defineSite` input may carry it to fail fast when a future revision of the
|
|
6
|
+
* contract changes shape; today only this revision exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SITE_KIND = "texuli.site/v1";
|
|
9
|
+
export interface TexuliSitePageInput {
|
|
10
|
+
/** kebab-case path segment; the home page's slug is ignored in favor of `home`. */
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
/** ISO calendar date (YYYY-MM-DD) emitted as the sitemap `lastmod` when set. */
|
|
15
|
+
updated?: string;
|
|
16
|
+
/** Navigation label; a page with one joins `site.navigation` in declared order. */
|
|
17
|
+
nav?: string;
|
|
18
|
+
/** Marks the site's home page; exactly one page per site sets it. */
|
|
19
|
+
home?: boolean;
|
|
20
|
+
/** Page content validated against the closed Texuli block schema. */
|
|
21
|
+
blocks?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface TexuliSiteInput {
|
|
24
|
+
/** Contract discriminator; when present it must be `texuli.site/v1`. */
|
|
25
|
+
kind?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
/** Deployed origin plus optional base path, without a trailing slash. */
|
|
28
|
+
url: string;
|
|
29
|
+
/** BCP 47-ish document language; defaults to `en`. */
|
|
30
|
+
lang?: string;
|
|
31
|
+
pages: TexuliSitePageInput[];
|
|
32
|
+
}
|
|
33
|
+
export interface TexuliSitePage {
|
|
34
|
+
slug: string;
|
|
35
|
+
/** Root path for the home page, otherwise `/${slug}`. */
|
|
36
|
+
path: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
updated?: string;
|
|
40
|
+
nav?: string;
|
|
41
|
+
home: boolean;
|
|
42
|
+
/** Absolute same-origin URL built from the site base. */
|
|
43
|
+
href: string;
|
|
44
|
+
blocks: Block[];
|
|
45
|
+
}
|
|
46
|
+
export interface TexuliSiteNavEntry {
|
|
47
|
+
slug: string;
|
|
48
|
+
label: string;
|
|
49
|
+
href: string;
|
|
50
|
+
}
|
|
51
|
+
export interface TexuliSite {
|
|
52
|
+
name: string;
|
|
53
|
+
url: string;
|
|
54
|
+
lang: string;
|
|
55
|
+
pages: TexuliSitePage[];
|
|
56
|
+
navigation: TexuliSiteNavEntry[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validate a static-site definition and return the frozen, normalized model
|
|
60
|
+
* that Protoyard `site` exports hand to the layouts, navigation and sitemap
|
|
61
|
+
* helpers. Throws a `TypeError` naming the offending field when any part of
|
|
62
|
+
* the closed schema fails: one home page, unique lowercase kebab-case slugs,
|
|
63
|
+
* an http(s) base URL without query or fragment, and page content that
|
|
64
|
+
* passes the closed Texuli block schema. Blocks are validated but never
|
|
65
|
+
* rendered here; painting them stays with the host's sink-free renderer.
|
|
66
|
+
*/
|
|
67
|
+
export declare function defineSite(input: TexuliSiteInput): TexuliSite;
|
|
68
|
+
/** Return the site's page for a slug, or `undefined` when absent. */
|
|
69
|
+
export declare function sitePage(site: TexuliSite, slug: string): TexuliSitePage | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Metadata for a page as ContentLayout props: the home page titles as the
|
|
72
|
+
* site name, others as `page title — site name`; the canonical is the
|
|
73
|
+
* page's absolute URL.
|
|
74
|
+
*/
|
|
75
|
+
export declare function sitePageMeta(site: TexuliSite, slug: string): {
|
|
76
|
+
title: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
canonical: string;
|
|
79
|
+
lang: string;
|
|
80
|
+
} | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Deterministic sitemap XML for the site: one entry per page in declared
|
|
83
|
+
* order, with `lastmod` only for pages that declare an `updated` date. The
|
|
84
|
+
* result is data for a `sitemap.xml` endpoint, never HTML.
|
|
85
|
+
*/
|
|
86
|
+
export declare function siteSitemap(site: TexuliSite): string;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { safeParseBlocks } from '@texuli/blocks/core';
|
|
2
|
+
import { safeHref } from '@texuli/core/url-policy';
|
|
3
|
+
/**
|
|
4
|
+
* The site kind identifier that Protoyard static-site exports declare. A
|
|
5
|
+
* `defineSite` input may carry it to fail fast when a future revision of the
|
|
6
|
+
* contract changes shape; today only this revision exists.
|
|
7
|
+
*/
|
|
8
|
+
export const SITE_KIND = 'texuli.site/v1';
|
|
9
|
+
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
11
|
+
const LANG_PATTERN = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new TypeError(`Invalid Texuli site: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
function text(value, field, max) {
|
|
16
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
17
|
+
fail(`${field} must be a non-empty string.`);
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
if (trimmed.length > max)
|
|
20
|
+
fail(`${field} must be at most ${max} characters.`);
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
function optionalText(value, field, max) {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
return text(value, field, max);
|
|
27
|
+
}
|
|
28
|
+
function xmlEscape(value) {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/&/g, '&')
|
|
31
|
+
.replace(/</g, '<')
|
|
32
|
+
.replace(/>/g, '>')
|
|
33
|
+
.replace(/"/g, '"')
|
|
34
|
+
.replace(/'/g, ''');
|
|
35
|
+
}
|
|
36
|
+
function normalizeBaseUrl(value) {
|
|
37
|
+
const raw = text(value, 'url', 2048);
|
|
38
|
+
// Site definitions are developer-authored, so the developer tier applies;
|
|
39
|
+
// the scheme check below still narrows to deployable http(s) origins.
|
|
40
|
+
const safe = safeHref(raw, 'developer');
|
|
41
|
+
if (safe === undefined)
|
|
42
|
+
fail('url must be a safe http(s) URL.');
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(safe);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
fail('url must be an absolute http(s) URL.');
|
|
49
|
+
}
|
|
50
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
51
|
+
fail('url must use the https or http scheme.');
|
|
52
|
+
}
|
|
53
|
+
if (parsed.search !== '')
|
|
54
|
+
fail('url must not carry a query string.');
|
|
55
|
+
if (parsed.hash !== '')
|
|
56
|
+
fail('url must not carry a fragment.');
|
|
57
|
+
const base = `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}`;
|
|
58
|
+
return base;
|
|
59
|
+
}
|
|
60
|
+
function joinHref(base, path) {
|
|
61
|
+
return path === '/' ? `${base}/` : `${base}${path}`;
|
|
62
|
+
}
|
|
63
|
+
function validateBlocks(slug, value) {
|
|
64
|
+
const parsed = safeParseBlocks(value);
|
|
65
|
+
if (parsed.success)
|
|
66
|
+
return parsed.data;
|
|
67
|
+
const summarize = (issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`;
|
|
68
|
+
const listed = parsed.issues.slice(0, 8).map(summarize).join('; ');
|
|
69
|
+
const more = parsed.issues.length > 8 ? ` (+${parsed.issues.length - 8} more)` : '';
|
|
70
|
+
fail(`page "${slug}" has invalid blocks — ${listed}${more}`);
|
|
71
|
+
}
|
|
72
|
+
function validatePage(input, base, seenSlugs) {
|
|
73
|
+
if (typeof input !== 'object' || input === null)
|
|
74
|
+
fail('every page must be an object.');
|
|
75
|
+
const slug = text(input.slug, 'page slug', 100);
|
|
76
|
+
if (!SLUG_PATTERN.test(slug)) {
|
|
77
|
+
fail(`page slug "${slug}" must be lowercase kebab-case (a-z, 0-9, dashes).`);
|
|
78
|
+
}
|
|
79
|
+
if (seenSlugs.has(slug))
|
|
80
|
+
fail(`page slug "${slug}" is duplicated.`);
|
|
81
|
+
seenSlugs.add(slug);
|
|
82
|
+
const title = text(input.title, `page "${slug}" title`, 200);
|
|
83
|
+
const description = optionalText(input.description, `page "${slug}" description`, 300);
|
|
84
|
+
const nav = optionalText(input.nav, `page "${slug}" nav label`, 120);
|
|
85
|
+
let updated;
|
|
86
|
+
if (input.updated !== undefined) {
|
|
87
|
+
updated = text(input.updated, `page "${slug}" updated`, 10);
|
|
88
|
+
if (!DATE_PATTERN.test(updated) || Number.isNaN(Date.parse(`${updated}T00:00:00Z`))) {
|
|
89
|
+
fail(`page "${slug}" updated must be an ISO calendar date (YYYY-MM-DD).`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const home = input.home === true;
|
|
93
|
+
const path = home ? '/' : `/${slug}`;
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
slug,
|
|
96
|
+
path,
|
|
97
|
+
title,
|
|
98
|
+
...(description === undefined ? {} : { description }),
|
|
99
|
+
...(updated === undefined ? {} : { updated }),
|
|
100
|
+
...(nav === undefined ? {} : { nav }),
|
|
101
|
+
home,
|
|
102
|
+
href: joinHref(base, path),
|
|
103
|
+
blocks: validateBlocks(slug, input.blocks ?? []),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function deepFreezeSite(site) {
|
|
107
|
+
for (const page of site.pages)
|
|
108
|
+
Object.freeze(page);
|
|
109
|
+
Object.freeze(site.pages);
|
|
110
|
+
Object.freeze(site.navigation);
|
|
111
|
+
return Object.freeze(site);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate a static-site definition and return the frozen, normalized model
|
|
115
|
+
* that Protoyard `site` exports hand to the layouts, navigation and sitemap
|
|
116
|
+
* helpers. Throws a `TypeError` naming the offending field when any part of
|
|
117
|
+
* the closed schema fails: one home page, unique lowercase kebab-case slugs,
|
|
118
|
+
* an http(s) base URL without query or fragment, and page content that
|
|
119
|
+
* passes the closed Texuli block schema. Blocks are validated but never
|
|
120
|
+
* rendered here; painting them stays with the host's sink-free renderer.
|
|
121
|
+
*/
|
|
122
|
+
export function defineSite(input) {
|
|
123
|
+
if (typeof input !== 'object' || input === null)
|
|
124
|
+
fail('the site definition must be an object.');
|
|
125
|
+
if (input.kind !== undefined && input.kind !== SITE_KIND) {
|
|
126
|
+
fail(`kind must be "${SITE_KIND}" when present.`);
|
|
127
|
+
}
|
|
128
|
+
const name = text(input.name, 'name', 120);
|
|
129
|
+
const base = normalizeBaseUrl(input.url);
|
|
130
|
+
const lang = input.lang === undefined ? 'en' : text(input.lang, 'lang', 35);
|
|
131
|
+
if (!LANG_PATTERN.test(lang))
|
|
132
|
+
fail('lang must be a base language tag with optional subtags.');
|
|
133
|
+
if (!Array.isArray(input.pages))
|
|
134
|
+
fail('pages must be an array.');
|
|
135
|
+
if (input.pages.length === 0)
|
|
136
|
+
fail('a site needs at least one page.');
|
|
137
|
+
const seenSlugs = new Set();
|
|
138
|
+
const pages = input.pages.map((page) => validatePage(page, base, seenSlugs));
|
|
139
|
+
const homes = pages.filter((page) => page.home);
|
|
140
|
+
if (homes.length === 0)
|
|
141
|
+
fail('exactly one page must set home: true.');
|
|
142
|
+
if (homes.length > 1)
|
|
143
|
+
fail('only one page may set home: true.');
|
|
144
|
+
const navigation = pages
|
|
145
|
+
.filter((page) => page.nav !== undefined)
|
|
146
|
+
.map((page) => Object.freeze({ slug: page.slug, label: page.nav, href: page.href }));
|
|
147
|
+
return deepFreezeSite({ name, url: base, lang, pages, navigation });
|
|
148
|
+
}
|
|
149
|
+
/** Return the site's page for a slug, or `undefined` when absent. */
|
|
150
|
+
export function sitePage(site, slug) {
|
|
151
|
+
return site.pages.find((page) => page.slug === slug);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Metadata for a page as ContentLayout props: the home page titles as the
|
|
155
|
+
* site name, others as `page title — site name`; the canonical is the
|
|
156
|
+
* page's absolute URL.
|
|
157
|
+
*/
|
|
158
|
+
export function sitePageMeta(site, slug) {
|
|
159
|
+
const page = sitePage(site, slug);
|
|
160
|
+
if (page === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
return {
|
|
163
|
+
title: page.home ? site.name : `${page.title} — ${site.name}`,
|
|
164
|
+
...(page.description === undefined ? {} : { description: page.description }),
|
|
165
|
+
canonical: page.href,
|
|
166
|
+
lang: site.lang,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Deterministic sitemap XML for the site: one entry per page in declared
|
|
171
|
+
* order, with `lastmod` only for pages that declare an `updated` date. The
|
|
172
|
+
* result is data for a `sitemap.xml` endpoint, never HTML.
|
|
173
|
+
*/
|
|
174
|
+
export function siteSitemap(site) {
|
|
175
|
+
const entries = site.pages
|
|
176
|
+
.map((page) => {
|
|
177
|
+
const lastmod = page.updated === undefined ? '' : ` <lastmod>${page.updated}</lastmod>\n`;
|
|
178
|
+
return ` <url>\n <loc>${xmlEscape(page.href)}</loc>\n${lastmod} </url>`;
|
|
179
|
+
})
|
|
180
|
+
.join('\n');
|
|
181
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries}\n</urlset>\n`;
|
|
182
|
+
}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@import "@texuli/tokens/base.css";
|
|
2
|
+
@import "@texuli/tokens/forced-colors.css";
|
|
3
|
+
|
|
4
|
+
/* Integration-selected variants override per-theme density and shape scales. */
|
|
5
|
+
:root[data-tx-density="compact"] {
|
|
6
|
+
--tx-density: 0.875;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root[data-tx-density="comfortable"] {
|
|
10
|
+
--tx-density: 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:root[data-tx-density="spacious"] {
|
|
14
|
+
--tx-density: 1.125;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root[data-tx-radius="compact"] {
|
|
18
|
+
--tx-radius-scale: 0.75;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:root[data-tx-radius="comfortable"] {
|
|
22
|
+
--tx-radius-scale: 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:root[data-tx-radius="rounded"] {
|
|
26
|
+
--tx-radius-scale: 1.25;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media print {
|
|
30
|
+
:root {
|
|
31
|
+
color-scheme: light;
|
|
32
|
+
--tx-motion: 0;
|
|
33
|
+
--tx-shadow: transparent;
|
|
34
|
+
print-color-adjust: economy;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@texuli/astro",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-next.0",
|
|
4
|
+
"description": "Astro integration, content layouts and Svelte islands for Texuli sites.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./content": "./dist/content/index.js",
|
|
13
|
+
"./integration": "./dist/integration/index.js",
|
|
14
|
+
"./site": "./dist/site/index.js",
|
|
15
|
+
"./styles.css": {
|
|
16
|
+
"types": "./dist/styles.css.d.ts",
|
|
17
|
+
"default": "./dist/styles.css"
|
|
18
|
+
},
|
|
19
|
+
"./layouts/ContentLayout": "./dist/layouts/ContentLayout.js",
|
|
20
|
+
"./sections/Hero": "./dist/sections/Hero.js",
|
|
21
|
+
"./sections/Section": "./dist/sections/Section.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@texuli/blocks": "0.1.0-next.0",
|
|
28
|
+
"@texuli/core": "0.1.0-next.0",
|
|
29
|
+
"@texuli/tokens": "0.1.0-next.0",
|
|
30
|
+
"astro": "^7.4.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@astrojs/svelte": "^9.1.0",
|
|
34
|
+
"svelte": "^5.57.2"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"tag": "next"
|
|
39
|
+
}
|
|
40
|
+
}
|