@picasso-ai/delivery-spike-core 0.0.0-stage → 0.0.2
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/components/Badge.astro +4 -0
- package/components/Greeting.tsx +3 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8 -0
- package/dist/config/site-schema.d.ts +21 -0
- package/dist/config/site-schema.js +1 -0
- package/dist/config/variants.d.ts +2 -0
- package/dist/config/variants.js +1 -0
- package/dist/content.d.ts +15 -0
- package/dist/content.js +15 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +61 -0
- package/dist/lib/variant-css.d.ts +33 -0
- package/dist/lib/variant-css.js +96 -0
- package/dist/site.d.ts +4 -0
- package/dist/site.js +6 -0
- package/dist/split-variant-css.d.ts +15 -0
- package/dist/split-variant-css.js +34 -0
- package/package.json +56 -4
- package/routes/album.astro +34 -0
- package/src/components/card-looks/compact.css +24 -0
- package/src/components/card-looks/title-slide.css +50 -0
- package/src/components/card-looks/title-strip.css +8 -0
- package/src/components/menu-effects/borders-slide-in.css +43 -0
- package/src/components/menu-effects/bottom-dots-expand.css +31 -0
- package/src/components/menu-effects/bottom-line-drop.css +31 -0
- package/src/components/menu-effects/bottom-line-slide-in.css +30 -0
- package/src/components/menu-effects/circles-expand.css +52 -0
- package/src/components/menu-effects/opacity-change.css +22 -0
- package/src/components/menu-effects/text-bounce-in.css +15 -0
- package/src/components/menu-effects/text-grow.css +15 -0
- package/src/components/menu-effects/word-slide-in.css +29 -0
- package/src/components/portfolio/album-layouts/crop.css +41 -0
- package/src/components/portfolio/album-layouts/justified.css +25 -0
- package/src/components/portfolio/album-layouts/masonry.css +18 -0
- package/src/components/portfolio/album-layouts/rect.css +8 -0
- package/src/components/portfolio/album-layouts/square.css +8 -0
- package/src/components/top-patterns/carbon.css +18 -0
- package/src/components/top-patterns/diagonal.css +13 -0
- package/src/components/top-patterns/hatch.css +18 -0
- package/src/components/top-patterns/plain.css +7 -0
- package/src/components/top-patterns/radial.css +13 -0
- package/src/components/top-patterns/weave.css +30 -0
- package/src/styles/layers.css +2 -0
- package/styles/core.css +5 -0
- package/README.md +0 -3
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readAlbumLayouts, selectVariantCss } from "./lib/variant-css.js";
|
|
4
|
+
import { loadSite } from "./site.js";
|
|
5
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
6
|
+
const siteRoot = process.argv[2] ?? process.cwd();
|
|
7
|
+
console.log(`${manifest.name}@${manifest.version} cli from ${import.meta.filename}`);
|
|
8
|
+
console.log(JSON.stringify(selectVariantCss(loadSite(siteRoot), readAlbumLayouts(siteRoot))));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AlbumLayout } from './variants.ts';
|
|
2
|
+
type CardLook = 'details' | 'compact' | 'title-strip' | 'title-slide';
|
|
3
|
+
export type SiteConfig = {
|
|
4
|
+
title: string;
|
|
5
|
+
menu: {
|
|
6
|
+
animate: string;
|
|
7
|
+
};
|
|
8
|
+
topSection: {
|
|
9
|
+
pattern: string;
|
|
10
|
+
};
|
|
11
|
+
album: {
|
|
12
|
+
layout: AlbumLayout;
|
|
13
|
+
};
|
|
14
|
+
portfolio: {
|
|
15
|
+
card: CardLook;
|
|
16
|
+
};
|
|
17
|
+
moreDocs: Record<string, {
|
|
18
|
+
card: CardLook;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ALBUM_LAYOUTS = ['justified', 'masonry', 'square', 'rect'];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
export declare const defineCoreCollections: () => {
|
|
3
|
+
albums: import("astro/content/config").CollectionConfig<z.ZodObject<{
|
|
4
|
+
title: z.ZodString;
|
|
5
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
6
|
+
justified: "justified";
|
|
7
|
+
masonry: "masonry";
|
|
8
|
+
square: "square";
|
|
9
|
+
rect: "rect";
|
|
10
|
+
}>>;
|
|
11
|
+
}, z.core.$strip>, import("astro/loaders").Loader>;
|
|
12
|
+
notes: import("astro/content/config").CollectionConfig<z.ZodObject<{
|
|
13
|
+
title: z.ZodString;
|
|
14
|
+
}, z.core.$strip>, import("astro/loaders").Loader>;
|
|
15
|
+
};
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineCollection } from 'astro:content';
|
|
2
|
+
import { glob } from 'astro/loaders';
|
|
3
|
+
import { z } from 'astro/zod';
|
|
4
|
+
import { ALBUM_LAYOUTS } from "./config/variants.js";
|
|
5
|
+
// the site's content.config.ts is one call of this builder; bases are relative to the site root
|
|
6
|
+
export const defineCoreCollections = () => ({
|
|
7
|
+
albums: defineCollection({
|
|
8
|
+
loader: glob({ pattern: '**/*.yaml', base: './src/content/albums' }),
|
|
9
|
+
schema: z.object({ title: z.string(), layout: z.enum(ALBUM_LAYOUTS).optional() }),
|
|
10
|
+
}),
|
|
11
|
+
notes: defineCollection({
|
|
12
|
+
loader: glob({ pattern: '**/*.mdx', base: './src/content/notes' }),
|
|
13
|
+
schema: z.object({ title: z.string() }),
|
|
14
|
+
}),
|
|
15
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import mdx from '@astrojs/mdx';
|
|
4
|
+
import react from '@astrojs/react';
|
|
5
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
6
|
+
import { variantCssPlugin } from "./lib/variant-css.js";
|
|
7
|
+
import { loadSite, siteConfigFile } from "./site.js";
|
|
8
|
+
import { splitVariantCssPlugin } from "./split-variant-css.js";
|
|
9
|
+
export const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
10
|
+
const SITE_MODULE = 'virtual:delivery-spike/site';
|
|
11
|
+
const RESOLVED_SITE_MODULE = `\0${SITE_MODULE}`;
|
|
12
|
+
function siteModulePlugin(siteRoot, customCss) {
|
|
13
|
+
return {
|
|
14
|
+
name: 'delivery-spike-site',
|
|
15
|
+
resolveId: (id) => (id === SITE_MODULE ? RESOLVED_SITE_MODULE : undefined),
|
|
16
|
+
load(id) {
|
|
17
|
+
if (id !== RESOLVED_SITE_MODULE)
|
|
18
|
+
return undefined;
|
|
19
|
+
const css = customCss.map((file) => `import ${JSON.stringify(path.resolve(siteRoot, file))}`);
|
|
20
|
+
const { title } = loadSite(siteRoot);
|
|
21
|
+
return [...css, `export const title = ${JSON.stringify(title)}`].join('\n');
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function variantPlugin(variantRoot, siteRoot) {
|
|
26
|
+
const shared = {
|
|
27
|
+
siteConfigFile: siteConfigFile(siteRoot),
|
|
28
|
+
loadSite: () => loadSite(siteRoot),
|
|
29
|
+
};
|
|
30
|
+
if (variantRoot === 'split')
|
|
31
|
+
return splitVariantCssPlugin({ packageRoot: PACKAGE_ROOT, siteRoot, ...shared });
|
|
32
|
+
const root = { cwd: process.cwd(), site: siteRoot, package: PACKAGE_ROOT }[variantRoot];
|
|
33
|
+
return variantCssPlugin({ root, ...shared });
|
|
34
|
+
}
|
|
35
|
+
export default function deliverySpikeCore(options = {}) {
|
|
36
|
+
return {
|
|
37
|
+
name: 'delivery-spike-core',
|
|
38
|
+
hooks: {
|
|
39
|
+
'astro:config:setup': ({ config, injectRoute, updateConfig, logger }) => {
|
|
40
|
+
const siteRoot = fileURLToPath(config.root);
|
|
41
|
+
const variantRoot = (process.env.SPIKE_VARIANT_ROOT ?? 'split');
|
|
42
|
+
logger.info(`package root ${PACKAGE_ROOT}`);
|
|
43
|
+
logger.info(`site root ${siteRoot}, cwd ${process.cwd()}, variant root ${variantRoot}`);
|
|
44
|
+
injectRoute({
|
|
45
|
+
pattern: '/albums/[slug]',
|
|
46
|
+
entrypoint: path.join(PACKAGE_ROOT, 'routes/album.astro'),
|
|
47
|
+
});
|
|
48
|
+
updateConfig({
|
|
49
|
+
integrations: [mdx(), react()],
|
|
50
|
+
vite: {
|
|
51
|
+
plugins: [
|
|
52
|
+
tailwindcss(),
|
|
53
|
+
siteModulePlugin(siteRoot, options.customCss ?? []),
|
|
54
|
+
variantPlugin(variantRoot, siteRoot),
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SiteConfig } from '../config/site-schema.ts';
|
|
2
|
+
import { type AlbumLayout } from '../config/variants.ts';
|
|
3
|
+
export declare const VARIANT_CSS_DIRS: {
|
|
4
|
+
readonly 'menu-effect': "src/components/menu-effects";
|
|
5
|
+
readonly 'top-pattern': "src/components/top-patterns";
|
|
6
|
+
readonly 'album-layout': "src/components/portfolio/album-layouts";
|
|
7
|
+
readonly 'card-look': "src/components/card-looks";
|
|
8
|
+
};
|
|
9
|
+
export type VariantCssFamily = keyof typeof VARIANT_CSS_DIRS;
|
|
10
|
+
export type VariantCssSite = Pick<SiteConfig, 'menu' | 'topSection' | 'album' | 'portfolio' | 'moreDocs'>;
|
|
11
|
+
export declare function selectVariantCss(site: VariantCssSite, albums: readonly {
|
|
12
|
+
layout?: AlbumLayout;
|
|
13
|
+
}[]): Record<VariantCssFamily, string[]>;
|
|
14
|
+
export declare function readAlbumLayouts(root: string): {
|
|
15
|
+
file: string;
|
|
16
|
+
layout?: AlbumLayout;
|
|
17
|
+
}[];
|
|
18
|
+
export declare const isVariantCssFamily: (value: string) => value is VariantCssFamily;
|
|
19
|
+
type PluginContext = {
|
|
20
|
+
addWatchFile(id: string): void;
|
|
21
|
+
};
|
|
22
|
+
export declare function everyVariantCss(root: string): Record<VariantCssFamily, string[]>;
|
|
23
|
+
export declare function variantCssPlugin(options: {
|
|
24
|
+
root: string;
|
|
25
|
+
siteConfigFile: string;
|
|
26
|
+
loadSite: () => VariantCssSite;
|
|
27
|
+
everyVariant?: boolean;
|
|
28
|
+
}): {
|
|
29
|
+
name: string;
|
|
30
|
+
resolveId(id: string): string | undefined;
|
|
31
|
+
load(this: PluginContext, id: string): string | undefined;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'astro/zod';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { ALBUM_LAYOUTS } from "../config/variants.js";
|
|
6
|
+
// astro bundles CSS by the route's module graph, not by what renders, so a component importing every
|
|
7
|
+
// variant stylesheet would ship them all. Each owner imports `virtual:variant-css/<family>` instead, and
|
|
8
|
+
// the plugin expands it into imports of the selected variants only.
|
|
9
|
+
const VIRTUAL_PREFIX = 'virtual:variant-css/';
|
|
10
|
+
const RESOLVED_PREFIX = `\0${VIRTUAL_PREFIX}`;
|
|
11
|
+
const ALBUMS_DIR = 'src/content/albums';
|
|
12
|
+
export const VARIANT_CSS_DIRS = {
|
|
13
|
+
'menu-effect': 'src/components/menu-effects',
|
|
14
|
+
'top-pattern': 'src/components/top-patterns',
|
|
15
|
+
'album-layout': 'src/components/portfolio/album-layouts',
|
|
16
|
+
'card-look': 'src/components/card-looks',
|
|
17
|
+
};
|
|
18
|
+
const files = (family, names) => [...new Set(names)].sort().map((name) => `${VARIANT_CSS_DIRS[family]}/${name}.css`);
|
|
19
|
+
// repo-relative stylesheets per family. A family takes the union of every config place that picks it:
|
|
20
|
+
// `off` has no file and `details` is the card base in card.css.
|
|
21
|
+
export function selectVariantCss(site, albums) {
|
|
22
|
+
const animate = site.menu.animate;
|
|
23
|
+
const pattern = site.topSection.pattern;
|
|
24
|
+
const looks = [site.portfolio.card, ...Object.values(site.moreDocs).map((more) => more.card)];
|
|
25
|
+
return {
|
|
26
|
+
'menu-effect': files('menu-effect', animate === 'off' ? [] : [animate]),
|
|
27
|
+
'top-pattern': files('top-pattern', pattern === 'off' ? [] : [pattern]),
|
|
28
|
+
'album-layout': files('album-layout', [
|
|
29
|
+
site.album.layout,
|
|
30
|
+
...albums.flatMap((album) => (album.layout ? [album.layout] : [])),
|
|
31
|
+
]),
|
|
32
|
+
// a title-slide card without a frame renders as title-strip (card.astro)
|
|
33
|
+
'card-look': files('card-look', looks.flatMap((look) => look === 'details' ? [] : look === 'title-slide' ? [look, 'title-strip'] : [look])),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const albumLayoutSchema = z.object({ layout: z.enum(ALBUM_LAYOUTS).optional() });
|
|
37
|
+
// reads only `layout` of every album; the full content snapshot is validated elsewhere
|
|
38
|
+
export function readAlbumLayouts(root) {
|
|
39
|
+
const directory = path.resolve(root, ALBUMS_DIR);
|
|
40
|
+
return readdirSync(directory, { recursive: true, encoding: 'utf8' })
|
|
41
|
+
.filter((name) => /\.ya?ml$/.test(name))
|
|
42
|
+
.sort()
|
|
43
|
+
.map((name) => {
|
|
44
|
+
const file = path.join(directory, name);
|
|
45
|
+
const result = albumLayoutSchema.safeParse(parse(readFileSync(file, 'utf8')));
|
|
46
|
+
if (!result.success)
|
|
47
|
+
throw new Error(`${file}: ${result.error.message}`);
|
|
48
|
+
return { file, ...result.data };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export const isVariantCssFamily = (value) => Object.hasOwn(VARIANT_CSS_DIRS, value);
|
|
52
|
+
const imports = (root, selected) => selected.length === 0
|
|
53
|
+
? 'export {}'
|
|
54
|
+
: selected.map((file) => `import ${JSON.stringify(path.resolve(root, file))}`).join('\n');
|
|
55
|
+
// every stylesheet of a family, shared ones included, for the E2E harness that renders unselected variants
|
|
56
|
+
export function everyVariantCss(root) {
|
|
57
|
+
const every = (family) => files(family, readdirSync(path.resolve(root, VARIANT_CSS_DIRS[family]))
|
|
58
|
+
.filter((name) => name.endsWith('.css'))
|
|
59
|
+
.map((name) => name.slice(0, -'.css'.length)));
|
|
60
|
+
return {
|
|
61
|
+
'menu-effect': every('menu-effect'),
|
|
62
|
+
'top-pattern': every('top-pattern'),
|
|
63
|
+
'album-layout': every('album-layout'),
|
|
64
|
+
'card-look': every('card-look'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// `loadSite` reads the config through the single loader (lib never imports it) and `siteConfigFile`
|
|
68
|
+
// is watched so a site.yaml or album edit re-selects the files in dev. `everyVariant` serves the E2E
|
|
69
|
+
// harness: its own imports would precede the owners' stylesheets (Astro orders page-level CSS first) and
|
|
70
|
+
// lose the equal-specificity overrides, so the variants ride the owners' imports instead.
|
|
71
|
+
export function variantCssPlugin(options) {
|
|
72
|
+
return {
|
|
73
|
+
name: 'variant-css',
|
|
74
|
+
resolveId(id) {
|
|
75
|
+
if (!id.startsWith(VIRTUAL_PREFIX))
|
|
76
|
+
return undefined;
|
|
77
|
+
const family = id.slice(VIRTUAL_PREFIX.length);
|
|
78
|
+
if (!isVariantCssFamily(family))
|
|
79
|
+
throw new Error(`unknown variant CSS family: ${family}`);
|
|
80
|
+
return `${RESOLVED_PREFIX}${family}`;
|
|
81
|
+
},
|
|
82
|
+
load(id) {
|
|
83
|
+
if (!id.startsWith(RESOLVED_PREFIX))
|
|
84
|
+
return undefined;
|
|
85
|
+
const family = id.slice(RESOLVED_PREFIX.length);
|
|
86
|
+
if (options.everyVariant)
|
|
87
|
+
return imports(options.root, everyVariantCss(options.root)[family]);
|
|
88
|
+
this.addWatchFile(options.siteConfigFile);
|
|
89
|
+
// only the album-layout module depends on the albums, so only it re-selects on their edits
|
|
90
|
+
const albums = family === 'album-layout' ? readAlbumLayouts(options.root) : [];
|
|
91
|
+
for (const album of albums)
|
|
92
|
+
this.addWatchFile(album.file);
|
|
93
|
+
return imports(options.root, selectVariantCss(options.loadSite(), albums)[family]);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/site.d.ts
ADDED
package/dist/site.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
export const SITE_CONFIG_FILE = 'site.yaml';
|
|
5
|
+
export const siteConfigFile = (siteRoot) => path.resolve(siteRoot, SITE_CONFIG_FILE);
|
|
6
|
+
export const loadSite = (siteRoot) => parse(readFileSync(siteConfigFile(siteRoot), 'utf8'));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type VariantCssSite } from './lib/variant-css.ts';
|
|
2
|
+
type PluginContext = {
|
|
3
|
+
addWatchFile(id: string): void;
|
|
4
|
+
};
|
|
5
|
+
export declare function splitVariantCssPlugin(options: {
|
|
6
|
+
packageRoot: string;
|
|
7
|
+
siteRoot: string;
|
|
8
|
+
siteConfigFile: string;
|
|
9
|
+
loadSite: () => VariantCssSite;
|
|
10
|
+
}): {
|
|
11
|
+
name: string;
|
|
12
|
+
resolveId(id: string): string | undefined;
|
|
13
|
+
load(this: PluginContext, id: string): string | undefined;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isVariantCssFamily, readAlbumLayouts, selectVariantCss, } from "./lib/variant-css.js";
|
|
3
|
+
const VIRTUAL_PREFIX = 'virtual:variant-css/';
|
|
4
|
+
const RESOLVED_PREFIX = `\0${VIRTUAL_PREFIX}`;
|
|
5
|
+
// the verbatim plugin resolves both the variant stylesheets and the albums from one `root`; here the
|
|
6
|
+
// stylesheets come from the installed package and the albums from the consumer site
|
|
7
|
+
export function splitVariantCssPlugin(options) {
|
|
8
|
+
return {
|
|
9
|
+
name: 'variant-css-split',
|
|
10
|
+
resolveId(id) {
|
|
11
|
+
if (!id.startsWith(VIRTUAL_PREFIX))
|
|
12
|
+
return undefined;
|
|
13
|
+
const family = id.slice(VIRTUAL_PREFIX.length);
|
|
14
|
+
if (!isVariantCssFamily(family))
|
|
15
|
+
throw new Error(`unknown variant CSS family: ${family}`);
|
|
16
|
+
return `${RESOLVED_PREFIX}${family}`;
|
|
17
|
+
},
|
|
18
|
+
load(id) {
|
|
19
|
+
if (!id.startsWith(RESOLVED_PREFIX))
|
|
20
|
+
return undefined;
|
|
21
|
+
const family = id.slice(RESOLVED_PREFIX.length);
|
|
22
|
+
this.addWatchFile(options.siteConfigFile);
|
|
23
|
+
const albums = family === 'album-layout' ? readAlbumLayouts(options.siteRoot) : [];
|
|
24
|
+
for (const album of albums)
|
|
25
|
+
this.addWatchFile(album.file);
|
|
26
|
+
const selected = selectVariantCss(options.loadSite(), albums)[family];
|
|
27
|
+
if (selected.length === 0)
|
|
28
|
+
return 'export {}';
|
|
29
|
+
return selected
|
|
30
|
+
.map((file) => `import ${JSON.stringify(path.resolve(options.packageRoot, file))}`)
|
|
31
|
+
.join('\n');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picasso-ai/delivery-spike-core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Throwaway delivery-channel spike package. Not for use.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/picasso/core-delivery-spike.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./content": {
|
|
18
|
+
"types": "./dist/content.d.ts",
|
|
19
|
+
"default": "./dist/content.js"
|
|
20
|
+
},
|
|
21
|
+
"./components/*": "./components/*",
|
|
22
|
+
"./core.css": "./styles/core.css"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"delivery-spike-core": "./dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"components",
|
|
30
|
+
"routes",
|
|
31
|
+
"styles",
|
|
32
|
+
"src/components",
|
|
33
|
+
"src/styles"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"astro": "^7.1.1",
|
|
37
|
+
"react": "^19.2.7",
|
|
38
|
+
"react-dom": "^19.2.7"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@astrojs/mdx": "^7.0.3",
|
|
42
|
+
"@astrojs/react": "^6.0.1",
|
|
43
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
44
|
+
"tailwindcss": "^4.3.3",
|
|
45
|
+
"yaml": "^2.9.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^24.0.0",
|
|
49
|
+
"@types/react": "^19.2.17",
|
|
50
|
+
"astro": "7.1.1",
|
|
51
|
+
"react": "19.2.7",
|
|
52
|
+
"react-dom": "19.2.7",
|
|
53
|
+
"typescript": "^6.0.3"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc -p tsconfig.json"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
import 'virtual:variant-css/album-layout'
|
|
3
|
+
import 'virtual:variant-css/card-look'
|
|
4
|
+
import 'virtual:variant-css/menu-effect'
|
|
5
|
+
import 'virtual:variant-css/top-pattern'
|
|
6
|
+
|
|
7
|
+
import { getCollection, getEntry, render } from 'astro:content'
|
|
8
|
+
import { title } from 'virtual:delivery-spike/site'
|
|
9
|
+
|
|
10
|
+
import Badge from '../components/Badge.astro'
|
|
11
|
+
import Greeting from '../components/Greeting.tsx'
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const albums = await getCollection('albums')
|
|
15
|
+
return albums.map((album) => ({ params: { slug: album.id }, props: { album } }))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { album } = Astro.props
|
|
19
|
+
const note = await getEntry('notes', album.id)
|
|
20
|
+
const rendered = note ? await render(note) : undefined
|
|
21
|
+
const Content = rendered?.Content
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<html lang="en">
|
|
25
|
+
<head>
|
|
26
|
+
<meta charset="utf-8" />
|
|
27
|
+
<title>{album.data.title} · {title}</title>
|
|
28
|
+
</head>
|
|
29
|
+
<body data-layout={album.data.layout ?? 'site-default'}>
|
|
30
|
+
<h1 class="core-route-heading">{album.data.title}</h1>
|
|
31
|
+
<Greeting name={title} />
|
|
32
|
+
{Content && <Content components={{ Badge }} />}
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* Only a Card with an explicit presentation sets these modifiers, so other collections are unaffected.
|
|
5
|
+
* Two lines at most so the strip does not cover the photo. */
|
|
6
|
+
.card--title-strip .card__title,
|
|
7
|
+
.card--title-slide .card__title {
|
|
8
|
+
display: -webkit-box;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
margin: 0;
|
|
11
|
+
font-size: 18px;
|
|
12
|
+
line-height: 1.3;
|
|
13
|
+
-webkit-box-orient: vertical;
|
|
14
|
+
-webkit-line-clamp: 2;
|
|
15
|
+
line-clamp: 2;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@media (max-width: 767px) {
|
|
19
|
+
.card--title-strip .card__title,
|
|
20
|
+
.card--title-slide .card__title {
|
|
21
|
+
font-size: 16px;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
@import './compact.css';
|
|
3
|
+
|
|
4
|
+
@layer components {
|
|
5
|
+
/* Dark translucent plate with white text in both themes: a theme-colored plate gets lost on a light
|
|
6
|
+
* sky, while this one stays ≈ 5.5:1 even on a white photo. Visible by default so touch needs no extra
|
|
7
|
+
* tap; hidden at rest only for a fine hover pointer. */
|
|
8
|
+
.card--title-slide .card__media-frame {
|
|
9
|
+
position: relative;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.card__overlay {
|
|
14
|
+
position: absolute;
|
|
15
|
+
inset-inline: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
padding: 10px 16px;
|
|
18
|
+
background-color: color-mix(in srgb, var(--color-overlay) 62%, transparent);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.card--title-slide .card__title,
|
|
22
|
+
.card--title-slide:hover .card__title,
|
|
23
|
+
.card--title-slide:focus-visible .card__title {
|
|
24
|
+
color: var(--color-on-image);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* The moving plate is feedback enough; dimming the photo too would be redundant. */
|
|
28
|
+
.card--title-slide:hover .card__img,
|
|
29
|
+
.card--title-slide:focus-visible .card__img {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (hover: hover) and (pointer: fine) {
|
|
34
|
+
.card--title-slide .card__overlay {
|
|
35
|
+
transform: translateY(100%);
|
|
36
|
+
transition: transform var(--duration-base) var(--ease-out);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.card--title-slide:hover .card__overlay,
|
|
40
|
+
.card--title-slide:focus-visible .card__overlay {
|
|
41
|
+
transform: none;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@media (prefers-reduced-motion: reduce) {
|
|
46
|
+
.card--title-slide .card__overlay {
|
|
47
|
+
transition: none;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* Top and bottom borders slide toward the text. */
|
|
5
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link {
|
|
6
|
+
padding-top: 10px;
|
|
7
|
+
padding-bottom: 10px;
|
|
8
|
+
font-weight: var(--font-weight-semibold);
|
|
9
|
+
text-shadow: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link::before,
|
|
13
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link::after {
|
|
14
|
+
content: '';
|
|
15
|
+
position: absolute;
|
|
16
|
+
left: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 2px;
|
|
19
|
+
background-color: var(--menu-effect-color);
|
|
20
|
+
opacity: 0;
|
|
21
|
+
transition:
|
|
22
|
+
opacity var(--duration-base) linear,
|
|
23
|
+
transform var(--duration-base) linear;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link::before {
|
|
27
|
+
top: 0;
|
|
28
|
+
transform: translateY(-10px);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link::after {
|
|
32
|
+
bottom: 0;
|
|
33
|
+
transform: translateY(10px);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link:hover::before,
|
|
37
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link:hover::after,
|
|
38
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link:focus-visible::before,
|
|
39
|
+
nav[aria-label='Primary'].anim-borders-slide-in .top-menu-link:focus-visible::after {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
transform: translateY(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* A dot splits into three. */
|
|
5
|
+
nav[aria-label='Primary'].anim-bottom-dots-expand .top-menu-link {
|
|
6
|
+
padding-bottom: 5px;
|
|
7
|
+
transition: color calc(var(--duration-base) * 2) var(--ease-out);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
nav[aria-label='Primary'].anim-bottom-dots-expand .top-menu-link::before {
|
|
11
|
+
content: '•';
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 100%;
|
|
14
|
+
left: 50%;
|
|
15
|
+
color: transparent;
|
|
16
|
+
font-size: 1.2em;
|
|
17
|
+
transform: translateX(-50%);
|
|
18
|
+
transition:
|
|
19
|
+
text-shadow var(--duration-base) linear,
|
|
20
|
+
color var(--duration-base) linear;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
nav[aria-label='Primary'].anim-bottom-dots-expand .top-menu-link:hover::before,
|
|
25
|
+
nav[aria-label='Primary'].anim-bottom-dots-expand .top-menu-link:focus-visible::before {
|
|
26
|
+
color: var(--menu-effect-color);
|
|
27
|
+
text-shadow:
|
|
28
|
+
15px 0 var(--menu-effect-color),
|
|
29
|
+
-15px 0 var(--menu-effect-color);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* The underline grows down from the top. */
|
|
5
|
+
nav[aria-label='Primary'].anim-bottom-line-drop .top-menu-link {
|
|
6
|
+
padding-bottom: 10px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
nav[aria-label='Primary'].anim-bottom-line-drop .top-menu-link::after {
|
|
10
|
+
content: '';
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 100%;
|
|
13
|
+
left: 0;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 1px;
|
|
16
|
+
background: var(--menu-effect-color);
|
|
17
|
+
opacity: 0;
|
|
18
|
+
transform: translateY(-10px);
|
|
19
|
+
transition:
|
|
20
|
+
height var(--duration-base) linear,
|
|
21
|
+
opacity var(--duration-base) linear,
|
|
22
|
+
transform var(--duration-base) linear;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
nav[aria-label='Primary'].anim-bottom-line-drop .top-menu-link:hover::after,
|
|
26
|
+
nav[aria-label='Primary'].anim-bottom-line-drop .top-menu-link:focus-visible::after {
|
|
27
|
+
height: 5px;
|
|
28
|
+
opacity: 1;
|
|
29
|
+
transform: translateY(0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* The underline slides in from below. */
|
|
5
|
+
nav[aria-label='Primary'].anim-bottom-line-slide-in .top-menu-link {
|
|
6
|
+
padding-top: 10px;
|
|
7
|
+
padding-bottom: 10px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
nav[aria-label='Primary'].anim-bottom-line-slide-in .top-menu-link::after {
|
|
11
|
+
content: '';
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 100%;
|
|
14
|
+
left: 0;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 5px;
|
|
17
|
+
background: var(--menu-effect-color);
|
|
18
|
+
opacity: 0;
|
|
19
|
+
transform: translateY(10px);
|
|
20
|
+
transition:
|
|
21
|
+
opacity var(--duration-base) linear,
|
|
22
|
+
transform var(--duration-base) linear;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
nav[aria-label='Primary'].anim-bottom-line-slide-in .top-menu-link:hover::after,
|
|
26
|
+
nav[aria-label='Primary'].anim-bottom-line-slide-in .top-menu-link:focus-visible::after {
|
|
27
|
+
opacity: 1;
|
|
28
|
+
transform: translateY(0);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* Two circles expand from the center. */
|
|
5
|
+
nav[aria-label='Primary'].anim-circles-expand .top-menu-label {
|
|
6
|
+
position: relative;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
nav[aria-label='Primary'].anim-circles-expand .top-menu-label::before,
|
|
10
|
+
nav[aria-label='Primary'].anim-circles-expand .top-menu-label::after {
|
|
11
|
+
content: '';
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 50%;
|
|
14
|
+
left: 50%;
|
|
15
|
+
width: 50px;
|
|
16
|
+
height: 50px;
|
|
17
|
+
border: 2px solid var(--menu-effect-color);
|
|
18
|
+
border-radius: 50%;
|
|
19
|
+
opacity: 0;
|
|
20
|
+
transform: translate(-50%, -50%) scale(0.2);
|
|
21
|
+
transition:
|
|
22
|
+
opacity var(--duration-base) linear,
|
|
23
|
+
transform var(--duration-base) linear;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
nav[aria-label='Primary'].anim-circles-expand .top-menu-label::after {
|
|
28
|
+
width: 40px;
|
|
29
|
+
height: 40px;
|
|
30
|
+
border-width: 5px;
|
|
31
|
+
transform: translate(-50%, -50%) scale(0.8);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* In light this effect alone draws the circles in translucent black, not the hover color, matching the
|
|
35
|
+
* legacy site. */
|
|
36
|
+
[data-theme='light'] nav[aria-label='Primary'].anim-circles-expand .top-menu-label::before,
|
|
37
|
+
[data-theme='light'] nav[aria-label='Primary'].anim-circles-expand .top-menu-label::after {
|
|
38
|
+
border-color: color-mix(in srgb, var(--color-shade) 15%, transparent);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
nav[aria-label='Primary'].anim-circles-expand .top-menu-link:hover .top-menu-label::before,
|
|
42
|
+
nav[aria-label='Primary'].anim-circles-expand .top-menu-link:hover .top-menu-label::after,
|
|
43
|
+
nav[aria-label='Primary'].anim-circles-expand
|
|
44
|
+
.top-menu-link:focus-visible
|
|
45
|
+
.top-menu-label::before,
|
|
46
|
+
nav[aria-label='Primary'].anim-circles-expand
|
|
47
|
+
.top-menu-link:focus-visible
|
|
48
|
+
.top-menu-label::after {
|
|
49
|
+
opacity: 1;
|
|
50
|
+
transform: translate(-50%, -50%) scale(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* A dimmed link turns fully opaque. */
|
|
5
|
+
nav[aria-label='Primary'].anim-opacity-change .top-menu-link {
|
|
6
|
+
opacity: 0.6;
|
|
7
|
+
transition:
|
|
8
|
+
opacity var(--duration-medium) var(--ease-cubic),
|
|
9
|
+
color var(--duration-medium) var(--ease-cubic);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
nav[aria-label='Primary'].anim-opacity-change .top-menu-link:hover,
|
|
13
|
+
nav[aria-label='Primary'].anim-opacity-change .top-menu-link:focus-visible {
|
|
14
|
+
opacity: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (prefers-reduced-motion: reduce) {
|
|
18
|
+
nav[aria-label='Primary'].anim-opacity-change .top-menu-link {
|
|
19
|
+
opacity: 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* The text grows with a bounce. */
|
|
5
|
+
nav[aria-label='Primary'].anim-text-bounce-in .top-menu-label {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
transform: scale(1);
|
|
8
|
+
transition: transform var(--duration-medium) cubic-bezier(0.47, 2.02, 0.31, -0.36);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nav[aria-label='Primary'].anim-text-bounce-in .top-menu-link:hover .top-menu-label,
|
|
12
|
+
nav[aria-label='Primary'].anim-text-bounce-in .top-menu-link:focus-visible .top-menu-label {
|
|
13
|
+
transform: scale(1.08);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* The text grows smoothly. */
|
|
5
|
+
nav[aria-label='Primary'].anim-text-grow .top-menu-label {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
transform: scale(1);
|
|
8
|
+
transition: transform var(--duration-base) var(--ease-out);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nav[aria-label='Primary'].anim-text-grow .top-menu-link:hover .top-menu-label,
|
|
12
|
+
nav[aria-label='Primary'].anim-text-grow .top-menu-link:focus-visible .top-menu-label {
|
|
13
|
+
transform: scale(1.1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* A copy of the word from attr(data-hover) slides in from below. */
|
|
5
|
+
nav[aria-label='Primary'].anim-word-slide-in .top-menu-link {
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
height: 1.1em;
|
|
8
|
+
line-height: 1.1em;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nav[aria-label='Primary'].anim-word-slide-in .top-menu-label {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: inline-block;
|
|
14
|
+
transition: transform var(--duration-base) linear;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
nav[aria-label='Primary'].anim-word-slide-in .top-menu-label::before {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 110%;
|
|
20
|
+
left: 0;
|
|
21
|
+
content: attr(data-hover);
|
|
22
|
+
font-weight: var(--font-weight-semibold);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
nav[aria-label='Primary'].anim-word-slide-in .top-menu-link:hover .top-menu-label,
|
|
26
|
+
nav[aria-label='Primary'].anim-word-slide-in .top-menu-link:focus-visible .top-menu-label {
|
|
27
|
+
transform: translateY(-110%);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@import '../../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* The minimum cell width makes `auto-fill` give min(columns, max(minColumns, floor((W + gap) /
|
|
5
|
+
* (240 + gap)))) columns, the same formula as `sizes` (lib/portfolio/sizes.ts) and Gallery. The
|
|
6
|
+
* viewer shows the uncropped photo. */
|
|
7
|
+
.album-square,
|
|
8
|
+
.album-rect {
|
|
9
|
+
--album-tile-max: calc(
|
|
10
|
+
(100% - (var(--album-columns) - 1) * var(--album-gap)) / var(--album-columns)
|
|
11
|
+
);
|
|
12
|
+
--album-tile-min: calc(
|
|
13
|
+
(100% - (var(--album-min-columns) - 1) * var(--album-gap)) / var(--album-min-columns)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: repeat(
|
|
18
|
+
auto-fill,
|
|
19
|
+
minmax(max(var(--album-tile-max), min(240px, var(--album-tile-min))), 1fr)
|
|
20
|
+
);
|
|
21
|
+
gap: var(--album-gap, 12px);
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
list-style: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* The anchor holds the cell ratio: <img> carries the photo's inline aspect-ratio (no CLS), and with
|
|
28
|
+
* width and height both set aspect-ratio no longer applies, so the image fills the cell height. */
|
|
29
|
+
:is(.album-square__item, .album-rect__item) .album-photo {
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:is(.album-square__item, .album-rect__item) :is(.album-photo__media, .album-photo__img) {
|
|
34
|
+
height: 100%;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:is(.album-square__item, .album-rect__item) .album-photo__img {
|
|
38
|
+
object-fit: cover;
|
|
39
|
+
object-position: var(--album-focal, 50% 50%);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
@import '../../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* photo-item.astro sets flex-grow ∝ aspect-ratio and flex-basis ∝ aspect-ratio × row height inline per
|
|
5
|
+
* photo, which gives every photo in a row the same height without cropping. The `::after` with a large
|
|
6
|
+
* flex-grow takes the slack only on the incomplete last row, so its photos keep the target height. */
|
|
7
|
+
.album-justified {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: var(--justified-gap, 12px);
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
list-style: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.album-justified::after {
|
|
17
|
+
content: '';
|
|
18
|
+
flex-grow: 1000000;
|
|
19
|
+
flex-basis: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.album-justified__item {
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@import '../../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
/* Photos run top to bottom by column; DOM, keyboard and viewer order stay the album order. */
|
|
5
|
+
.album-masonry {
|
|
6
|
+
column-width: var(--album-col-width, 300px);
|
|
7
|
+
column-count: var(--album-max-columns, 5);
|
|
8
|
+
column-gap: var(--album-gap, 12px);
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
list-style: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.album-masonry__item {
|
|
15
|
+
break-inside: avoid;
|
|
16
|
+
margin-bottom: var(--album-gap, 12px);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.top-section--pattern-carbon .top-section__pattern::after {
|
|
5
|
+
background:
|
|
6
|
+
radial-gradient(
|
|
7
|
+
color-mix(in srgb, var(--color-shade) 15%, transparent) 30%,
|
|
8
|
+
transparent 40%
|
|
9
|
+
)
|
|
10
|
+
0 0,
|
|
11
|
+
radial-gradient(
|
|
12
|
+
color-mix(in srgb, var(--color-shade) 15%, transparent) 30%,
|
|
13
|
+
transparent 40%
|
|
14
|
+
)
|
|
15
|
+
3px 3px;
|
|
16
|
+
background-size: 6px 6px;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.top-section--pattern-diagonal .top-section__pattern::after {
|
|
5
|
+
background: repeating-linear-gradient(
|
|
6
|
+
-25deg,
|
|
7
|
+
color-mix(in srgb, var(--color-shade) 20%, transparent),
|
|
8
|
+
color-mix(in srgb, var(--color-shade) 20%, transparent) 3px,
|
|
9
|
+
color-mix(in srgb, var(--color-shade) 10%, transparent) 3px,
|
|
10
|
+
color-mix(in srgb, var(--color-shade) 10%, transparent) 6px
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.top-section--pattern-hatch .top-section__pattern::after {
|
|
5
|
+
background:
|
|
6
|
+
radial-gradient(
|
|
7
|
+
transparent 40%,
|
|
8
|
+
color-mix(in srgb, var(--color-shade) 20%, transparent) 41%
|
|
9
|
+
)
|
|
10
|
+
0 0,
|
|
11
|
+
radial-gradient(
|
|
12
|
+
transparent 40%,
|
|
13
|
+
color-mix(in srgb, var(--color-shade) 20%, transparent) 41%
|
|
14
|
+
)
|
|
15
|
+
3px 3px;
|
|
16
|
+
background-size: 6px 6px;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.top-section--pattern-plain .top-section__pattern::after {
|
|
5
|
+
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAEAQMAAACTPww9AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMAtyOW6xcAAAAQSURBVAjXYxBgUGBwYGgAAAJYAPF9gAMvAAAAAElFTkSuQmCC);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.top-section--pattern-radial .top-section__pattern::after {
|
|
5
|
+
background: repeating-radial-gradient(
|
|
6
|
+
circle,
|
|
7
|
+
color-mix(in srgb, var(--color-shade) 10%, transparent),
|
|
8
|
+
color-mix(in srgb, var(--color-shade) 10%, transparent) 2px,
|
|
9
|
+
transparent 2px,
|
|
10
|
+
transparent 5px
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@import '../../styles/layers.css';
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.top-section--pattern-weave .top-section__pattern::after {
|
|
5
|
+
background:
|
|
6
|
+
linear-gradient(
|
|
7
|
+
45deg,
|
|
8
|
+
color-mix(in srgb, var(--color-on-image) 10%, transparent) 12%,
|
|
9
|
+
transparent 0,
|
|
10
|
+
transparent 88%,
|
|
11
|
+
color-mix(in srgb, var(--color-on-image) 10%, transparent) 0
|
|
12
|
+
),
|
|
13
|
+
linear-gradient(
|
|
14
|
+
135deg,
|
|
15
|
+
transparent 37%,
|
|
16
|
+
color-mix(in srgb, var(--color-shade) 15%, transparent) 0,
|
|
17
|
+
color-mix(in srgb, var(--color-shade) 15%, transparent) 63%,
|
|
18
|
+
transparent 0
|
|
19
|
+
),
|
|
20
|
+
linear-gradient(
|
|
21
|
+
45deg,
|
|
22
|
+
transparent 37%,
|
|
23
|
+
color-mix(in srgb, var(--color-on-image) 10%, transparent) 0,
|
|
24
|
+
color-mix(in srgb, var(--color-on-image) 10%, transparent) 63%,
|
|
25
|
+
transparent 0
|
|
26
|
+
)
|
|
27
|
+
color-mix(in srgb, var(--color-shade) 20%, transparent);
|
|
28
|
+
background-size: 5px 5px;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/styles/core.css
ADDED
package/README.md
DELETED