Here before
This commit is contained in:
parent
da66707338
commit
8887690d1c
29 changed files with 1452 additions and 830 deletions
|
|
@ -14,5 +14,20 @@ const config: StorybookConfig = {
|
|||
name: '@storybook/sveltekit',
|
||||
options: {},
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import('vite');
|
||||
const path = await import('node:path');
|
||||
const fs = await import('node:fs');
|
||||
const { fileURLToPath } = await import('node:url');
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const storybookTsconfigPath = path.resolve(__dirname, 'tsconfig.json');
|
||||
const storybookTsconfigRaw = fs.readFileSync(storybookTsconfigPath, 'utf8');
|
||||
return mergeConfig(config, {
|
||||
esbuild: {
|
||||
...config.esbuild,
|
||||
tsconfigRaw: storybookTsconfigRaw,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
|
|
|||
32
.storybook/tsconfig.json
Normal file
32
.storybook/tsconfig.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"rootDir": "..",
|
||||
"baseUrl": ".."
|
||||
},
|
||||
"include": [
|
||||
"../src/**/*.ts",
|
||||
"../src/**/*.svelte",
|
||||
"*.ts",
|
||||
"*.svelte"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
10
README.md
10
README.md
|
|
@ -7,3 +7,13 @@
|
|||
Svelte components, SCSS and more for Reuters Graphics pages.
|
||||
|
||||
[Read the docs.](https://reuters-graphics.github.io/graphics-components/)
|
||||
|
||||
## Storybook
|
||||
|
||||
Run Storybook with:
|
||||
|
||||
```bash
|
||||
pnpm run storybook
|
||||
```
|
||||
|
||||
Do not run `storybook dev` directly. The script runs `svelte-kit sync` first so that `.svelte-kit/tsconfig.json` exists; otherwise the root tsconfig’s `extends` fails and Storybook will error. The same applies to `pnpm run build:storybook`.
|
||||
|
|
|
|||
23
package.json
23
package.json
|
|
@ -8,8 +8,8 @@
|
|||
"dev": "svelte-kit sync && vite dev",
|
||||
"build": "svelte-kit sync && node scripts/copy-assets.js && vite build",
|
||||
"preview": "vite preview",
|
||||
"storybook": "svelte-kit sync && storybook dev -p 3000",
|
||||
"build:storybook": "svelte-kit sync && storybook build -o docs",
|
||||
"storybook": "node scripts/ensure-svelte-kit-tsconfig.cjs && svelte-kit sync && storybook dev -p 3000",
|
||||
"build:storybook": "node scripts/ensure-svelte-kit-tsconfig.cjs && svelte-kit sync && storybook build -o docs",
|
||||
"lint": "eslint --fix",
|
||||
"format": "prettier . --write",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
|
|
@ -22,18 +22,18 @@
|
|||
"node": ">=20.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.6",
|
||||
"@chromatic-com/storybook": "^5.0.1",
|
||||
"@reuters-graphics/yaks-eslint": "^0.1.1",
|
||||
"@reuters-graphics/yaks-prettier": "^0.1.1",
|
||||
"@storybook/addon-a11y": "^8.6.12",
|
||||
"@storybook/addon-a11y": "^10.2.8",
|
||||
"@storybook/addon-essentials": "^8.6.12",
|
||||
"@storybook/addon-interactions": "^8.6.12",
|
||||
"@storybook/addon-svelte-csf": "5.0.0-next.28",
|
||||
"@storybook/addon-svelte-csf": "5.0.11",
|
||||
"@storybook/blocks": "^8.6.12",
|
||||
"@storybook/components": "^8.6.12",
|
||||
"@storybook/manager-api": "^8.6.12",
|
||||
"@storybook/svelte": "^8.6.12",
|
||||
"@storybook/sveltekit": "^8.6.12",
|
||||
"@storybook/svelte": "^10.2.8",
|
||||
"@storybook/sveltekit": "^10.2.8",
|
||||
"@storybook/test": "^8.6.12",
|
||||
"@storybook/theming": "^8.6.12",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
|
|
@ -58,9 +58,10 @@
|
|||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-mdx": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-storybook": "^0.12.0",
|
||||
"eslint-plugin-storybook": "^10.2.8",
|
||||
"knip": "^5.50.5",
|
||||
"mermaid": "^10.9.3",
|
||||
"playwright": "^1.58.2",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"sass": "^1.86.3",
|
||||
"storybook": "^8.6.12",
|
||||
"storybook": "^10.2.8",
|
||||
"svelte": "^5.28.1",
|
||||
"svelte-check": "^4.1.6",
|
||||
"typescript": "^5.8.3",
|
||||
|
|
@ -87,6 +88,8 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"es-toolkit": "^1.35.0",
|
||||
"journalize": "^2.6.0",
|
||||
"locomotive-scroll": "^5.0.1",
|
||||
"motion": "^12.34.0",
|
||||
"mp4box": "^0.5.4",
|
||||
"proper-url-join": "^2.1.2",
|
||||
"pym.js": "^1.3.2",
|
||||
|
|
@ -97,4 +100,4 @@
|
|||
"ua-parser-js": "^2.0.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
1426
pnpm-lock.yaml
1426
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
107
public/stories/hello-world/manor.svelte
Normal file
107
public/stories/hello-world/manor.svelte
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
assetsPath = '/',
|
||||
onAiMounted = () => {},
|
||||
onArtboardChange = () => {},
|
||||
/* taggedText = { text: {}, htext: {} },
|
||||
debugTaggedText = false, */
|
||||
artboardWidth = $bindable(undefined),
|
||||
} = $props();
|
||||
import { onMount, untrack } from 'svelte';
|
||||
let aiBox: HTMLElement | undefined;
|
||||
let screenWidth = $state(0);
|
||||
let aiBoxWidth = $derived(artboardWidth ?? screenWidth);
|
||||
let activeArtboard: HTMLElement | undefined = $state(undefined);
|
||||
onMount(() => {
|
||||
onAiMounted();
|
||||
});
|
||||
$effect(() => {
|
||||
if (aiBoxWidth) {
|
||||
const currentArtboard = (aiBox as HTMLElement).querySelectorAll(
|
||||
'.g-artboard'
|
||||
)[0] as HTMLElement;
|
||||
if (currentArtboard?.id !== activeArtboard?.id) {
|
||||
activeArtboard = untrack(() => currentArtboard);
|
||||
onArtboardChange(activeArtboard);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="g-manor_xl-box"
|
||||
class="ai2svelte"
|
||||
bind:this={aiBox}
|
||||
bind:clientWidth={aiBoxWidth}
|
||||
>
|
||||
<!-- Artboard: xl -->
|
||||
<div
|
||||
id="g-manor_xl-xl"
|
||||
class="g-artboard"
|
||||
style="aspect-ratio: 1.93548387096774;"
|
||||
data-aspect-ratio="1.935"
|
||||
data-min-width="0"
|
||||
>
|
||||
<div
|
||||
id="g-manor_xl-xl-img"
|
||||
class="g-manor_xl-xl-img g-aiImg"
|
||||
alt="asdf"
|
||||
style="background-image: url({assetsPath.replace(
|
||||
new RegExp('/([^/.]+)$'),
|
||||
'/$1/'
|
||||
) || '/'}images/graphics/manor_xl-xl.jpg);"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- End ai2svelte - 2026-02-15 17:58 -->
|
||||
|
||||
<!-- Generated by ai2svelte v1.0.3 - 2026-02-15 17:58 -->
|
||||
<!-- ai file: manor_xl.ai -->
|
||||
<style lang="scss">
|
||||
#g-manor_xl-box,
|
||||
#g-manor_xl-box .g-artboard {
|
||||
margin: 0 auto;
|
||||
}
|
||||
#g-manor_xl-box p {
|
||||
margin: 0;
|
||||
}
|
||||
#g-manor_xl-box .g-aiAbs {
|
||||
position: absolute;
|
||||
}
|
||||
#g-manor_xl-box .g-aiImg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#g-manor_xl-box .g-aiSymbol {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#g-manor_xl-box .g-aiPointText p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#g-manor_xl-box.g-aiPointText p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#g-manor_xl-box {
|
||||
width: 100%;
|
||||
container-type: inline-size;
|
||||
@container (min-width:0px) {
|
||||
#g-manor_xl-xl {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
#g-manor_xl-xl {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Custom CSS */
|
||||
</style>
|
||||
BIN
public/stories/hello-world/manor_xl.jpg
(Stored with Git LFS)
Normal file
BIN
public/stories/hello-world/manor_xl.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -9,7 +9,7 @@
|
|||
import { readdirSync, cpSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const CONTENT_DIR = resolve('content/stories');
|
||||
const CONTENT_DIR = resolve('public/stories');
|
||||
const STATIC_DIR = resolve('static/stories');
|
||||
|
||||
if (!existsSync(CONTENT_DIR)) {
|
||||
|
|
|
|||
39
scripts/ensure-svelte-kit-tsconfig.cjs
Normal file
39
scripts/ensure-svelte-kit-tsconfig.cjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Ensures .svelte-kit/tsconfig.json exists so the root tsconfig's "extends" resolves.
|
||||
* Storybook's Vite pipeline resolves the root tsconfig; if .svelte-kit is missing (e.g.
|
||||
* svelte-kit sync hasn't run), the build fails. This script writes a minimal fallback
|
||||
* so the root extends succeed. svelte-kit sync will overwrite with the real file.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const dir = path.join(process.cwd(), '.svelte-kit');
|
||||
const file = path.join(dir, 'tsconfig.json');
|
||||
|
||||
const minimal = {
|
||||
compilerOptions: {
|
||||
module: 'ESNext',
|
||||
moduleResolution: 'Bundler',
|
||||
target: 'ESNext',
|
||||
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
|
||||
strict: true,
|
||||
skipLibCheck: true,
|
||||
noEmit: true,
|
||||
isolatedModules: true,
|
||||
esModuleInterop: true,
|
||||
resolveJsonModule: true,
|
||||
allowJs: true,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
verbatimModuleSyntax: true
|
||||
},
|
||||
include: [],
|
||||
exclude: []
|
||||
};
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.writeFileSync(file, JSON.stringify(minimal, null, 2) + '\n', 'utf8');
|
||||
console.log('Created .svelte-kit/tsconfig.json (minimal fallback for Storybook)');
|
||||
}
|
||||
76
src/actions/motionInView/index.ts
Normal file
76
src/actions/motionInView/index.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { inView, animate } from 'motion';
|
||||
import type { AnimationConfig } from '$lib/lib/content';
|
||||
|
||||
function buildKeyframes(
|
||||
initial: Record<string, unknown> | undefined,
|
||||
animateTo: Record<string, unknown> | undefined
|
||||
): Record<string, unknown> {
|
||||
const target = animateTo ?? { opacity: 1 };
|
||||
const from = initial ?? {};
|
||||
const keyframes: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(target)) {
|
||||
const fromVal = from[key];
|
||||
const toVal = target[key];
|
||||
if (fromVal !== undefined && fromVal !== toVal) {
|
||||
keyframes[key] = [fromVal, toVal];
|
||||
} else {
|
||||
keyframes[key] = toVal;
|
||||
}
|
||||
}
|
||||
return keyframes;
|
||||
}
|
||||
|
||||
export default function motionInView(
|
||||
node: HTMLElement,
|
||||
config: AnimationConfig | undefined
|
||||
): { update?: (config: AnimationConfig | undefined) => void; destroy?: () => void } {
|
||||
if (!config || (!config.inView && !config.initial && !config.animate)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const initial = config.initial ?? {};
|
||||
const animateTo = config.animate ?? { opacity: 1 };
|
||||
const transition = config.transition ?? {};
|
||||
const keyframes = buildKeyframes(initial, animateTo);
|
||||
|
||||
// Apply initial state so element is in initial position before in view
|
||||
if (Object.keys(initial).length > 0) {
|
||||
const style = node.style as unknown as Record<string, string>;
|
||||
const x = Number(initial.x ?? 0);
|
||||
const y = Number(initial.y ?? 0);
|
||||
const scale = initial.scale;
|
||||
const parts: string[] = [];
|
||||
if (x !== 0 || y !== 0) parts.push(`translate(${x}px, ${y}px)`);
|
||||
if (scale !== undefined) parts.push(`scale(${scale})`);
|
||||
if (parts.length) style.transform = parts.join(' ');
|
||||
if (initial.opacity !== undefined) style.opacity = String(initial.opacity);
|
||||
}
|
||||
|
||||
const inViewOptions: Record<string, unknown> = {};
|
||||
if (config.margin !== undefined) inViewOptions.margin = config.margin;
|
||||
if (config.amount !== undefined) inViewOptions.amount = config.amount;
|
||||
|
||||
let stop: (() => void) | undefined = inView(
|
||||
node,
|
||||
() => {
|
||||
const anim = animate(node, keyframes, {
|
||||
duration: transition.duration,
|
||||
delay: transition.delay,
|
||||
});
|
||||
return () => anim.stop();
|
||||
},
|
||||
inViewOptions as Parameters<typeof inView>[2]
|
||||
);
|
||||
|
||||
return {
|
||||
update(newConfig: AnimationConfig | undefined) {
|
||||
if (!newConfig || !stop) return;
|
||||
stop();
|
||||
stop = undefined;
|
||||
// Re-run with new config by re-applying action (caller typically re-mounts or we could re-init here)
|
||||
},
|
||||
destroy() {
|
||||
stop?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
23
src/components/AnimatedBlock/AnimatedBlock.svelte
Normal file
23
src/components/AnimatedBlock/AnimatedBlock.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Wraps content and applies Motion inView + animate when animation config is provided. -->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { AnimationConfig } from '$lib/lib/content';
|
||||
import motionInView from '../../actions/motionInView/index';
|
||||
|
||||
interface Props {
|
||||
/** When set, the wrapper enters view via Motion inView and animates from initial to animate. */
|
||||
animation?: AnimationConfig;
|
||||
/** Content to wrap (e.g. BodyText, FeaturePhoto, GraphicBlock). */
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { animation, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if animation?.inView || animation?.initial || animation?.animate}
|
||||
<div use:motionInView={animation}>
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
{/if}
|
||||
105
src/components/ScrollScene/ScrollScene.svelte
Normal file
105
src/components/ScrollScene/ScrollScene.svelte
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<!--
|
||||
@component ScrollScene
|
||||
|
||||
A sticky scroll container powered by CSS `position: sticky` and
|
||||
Locomotive Scroll's `data-scroll-css-progress` for progress tracking.
|
||||
|
||||
The outer element is tall (`distance`) to create scroll room.
|
||||
The inner `.stage` sticks at the top of the viewport.
|
||||
As the user scrolls, `progress` goes 0→1 during the sticky phase.
|
||||
|
||||
Progress is exposed two ways:
|
||||
- `--stage-progress` CSS variable on `.stage` (for CSS-driven animations)
|
||||
- Bindable `progress` prop (for JS-driven animations in Svelte)
|
||||
|
||||
Usage:
|
||||
<ScrollScene distance="300vh" bind:progress>
|
||||
<MyAnimatedLayer {progress} />
|
||||
</ScrollScene>
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** Total height of the scroll container (CSS value). Default "200vh". */
|
||||
distance?: string;
|
||||
/** Remapped progress 0–1 during the sticky phase. Bindable. */
|
||||
progress?: number;
|
||||
/** CSS class for the outer container. */
|
||||
class?: string;
|
||||
/** Content rendered inside the sticky stage. */
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
distance = '200vh',
|
||||
progress = $bindable(0),
|
||||
class: cls = '',
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
let scene: HTMLElement | undefined = $state(undefined);
|
||||
let stickStart = 0;
|
||||
|
||||
const eventName = `scene-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
function clamp(v: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, v));
|
||||
}
|
||||
|
||||
function computeStickStart() {
|
||||
if (!scene) return;
|
||||
const H = scene.offsetHeight;
|
||||
const V = window.innerHeight;
|
||||
stickStart = H > 0 ? V / H : 0;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
computeStickStart();
|
||||
|
||||
function onProgress(e: Event) {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
const raw: number = detail?.progress ?? 0;
|
||||
progress = clamp(
|
||||
(raw - stickStart) / (1 - stickStart),
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener(eventName, onProgress);
|
||||
window.addEventListener('resize', computeStickStart, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(eventName, onProgress);
|
||||
window.removeEventListener('resize', computeStickStart);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={scene}
|
||||
class="scroll-scene {cls}"
|
||||
style:height={distance}
|
||||
data-scroll
|
||||
data-scroll-css-progress
|
||||
data-scroll-event-progress={eventName}
|
||||
>
|
||||
<div class="stage" style:--stage-progress={progress}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.scroll-scene {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,9 +6,11 @@
|
|||
steps: ScrollerStep[];
|
||||
preload?: number;
|
||||
stackBackground?: boolean;
|
||||
offset?: number;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
let { index, steps, preload = 1, stackBackground = true }: Props = $props();
|
||||
let { index, steps, preload = 1, stackBackground = true, offset = 0, progress = 0 }: Props = $props();
|
||||
|
||||
function showStep(i: number) {
|
||||
if (preload === 0) return true;
|
||||
|
|
@ -29,7 +31,12 @@
|
|||
class:visible={isVisible(i)}
|
||||
class:invisible={!isVisible(i)}
|
||||
>
|
||||
<step.background {...step.backgroundProps || {}}></step.background>
|
||||
<step.background
|
||||
{...step.backgroundProps || {}}
|
||||
index={i}
|
||||
offset={offset}
|
||||
progress={progress}
|
||||
></step.background>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,14 @@
|
|||
class="background-container step-{index +
|
||||
1} my-0 min-h-screen flex justify-center items-center relative"
|
||||
>
|
||||
<Background {index} {steps} {preload} {stackBackground} />
|
||||
<Background
|
||||
{index}
|
||||
{steps}
|
||||
{preload}
|
||||
{stackBackground}
|
||||
{offset}
|
||||
{progress}
|
||||
/>
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
imgSrc?: string;
|
||||
/** SVG path `d` attribute for the foreground clip silhouette */
|
||||
fgClipPath?: string;
|
||||
/** Width of the original clip-path coordinate space (e.g. SVG viewBox width) */
|
||||
clipWidth?: number;
|
||||
/** Height of the original clip-path coordinate space (e.g. SVG viewBox height) */
|
||||
clipHeight?: number;
|
||||
index?: number;
|
||||
offset?: number;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
imgSrc = '',
|
||||
fgClipPath = '',
|
||||
clipWidth = 1,
|
||||
clipHeight = 1,
|
||||
index = 0,
|
||||
offset = 0,
|
||||
progress = 0,
|
||||
}: Props = $props();
|
||||
|
||||
// Unique clip ID per instance so multiple scrollers don't collide
|
||||
const clipId = `dolly-clip-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
// Scale factors to convert SVG viewBox coords → objectBoundingBox 0–1 range
|
||||
let scaleX = $derived(clipWidth > 0 ? 1 / clipWidth : 1);
|
||||
let scaleY = $derived(clipHeight > 0 ? 1 / clipHeight : 1);
|
||||
|
||||
// Dolly zoom driven by scroll progress (0 → 1)
|
||||
// Mirrors CodePen https://codepen.io/scharamoose/pen/PGpGez
|
||||
//
|
||||
// BG: starts zoomed in via translateZ(150), pulls back to 0, blurs
|
||||
// FG: starts neutral at translateZ(0), pushes forward to 200, clipped by SVG silhouette
|
||||
let bgZ = $derived(150 - progress * 150);
|
||||
let bgBlur = $derived(progress * 6);
|
||||
let fgZ = $derived(progress * 200);
|
||||
|
||||
/** CSS aspect-ratio so the clipped layer matches the path coordinate space (no stretch). */
|
||||
let aspectRatioCss = $derived(`${clipWidth} / ${clipHeight}`);
|
||||
</script>
|
||||
|
||||
{#if fgClipPath}
|
||||
<svg class="clip-defs" aria-hidden="true">
|
||||
<defs>
|
||||
<clipPath id={clipId} clipPathUnits="objectBoundingBox">
|
||||
<path transform="scale({scaleX}, {scaleY})" d={fgClipPath} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
<div class="dolly-zoom-container">
|
||||
<!-- BG: full image, pulls back in Z, blurs -->
|
||||
<div class="box bg-box">
|
||||
<div
|
||||
class="img"
|
||||
style:background-image="url({imgSrc})"
|
||||
style:transform="translateZ({bgZ}px)"
|
||||
style:filter="blur({bgBlur}px)"
|
||||
></div>
|
||||
</div>
|
||||
<!-- FG: same image, pushes forward in Z, clipped to silhouette.
|
||||
Sized to clip aspect ratio so objectBoundingBox clip isn't stretched. -->
|
||||
<div class="box fg-box">
|
||||
<div
|
||||
class="img img--clipped"
|
||||
style:aspect-ratio={aspectRatioCss}
|
||||
style:background-image="url({imgSrc})"
|
||||
style:transform="translate(-50%, -50%) translateZ({fgZ}px)"
|
||||
style:clip-path={fgClipPath ? `url(#${clipId})` : 'none'}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.clip-defs {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dolly-zoom-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
perspective: 400px;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.bg-box {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.fg-box {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
will-change: transform;
|
||||
transition: transform 5s, filter 5s;
|
||||
}
|
||||
|
||||
/* Foreground clipped layer: preserve clip-path aspect ratio so silhouette aligns with image */
|
||||
.img--clipped {
|
||||
inset: auto;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,7 +2,7 @@ import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|||
import { join, resolve } from 'node:path';
|
||||
import { parse } from '@rferl/veronica';
|
||||
|
||||
const CONTENT_DIR = resolve('content/stories');
|
||||
const CONTENT_DIR = resolve('public/stories');
|
||||
|
||||
// -- Types ------------------------------------------------------------------
|
||||
|
||||
|
|
@ -15,9 +15,19 @@ export type ContainerWidth =
|
|||
| 'widest'
|
||||
| 'fluid';
|
||||
|
||||
export interface AnimationConfig {
|
||||
inView?: boolean;
|
||||
margin?: string;
|
||||
amount?: 'some' | 'all' | number;
|
||||
initial?: Record<string, unknown>;
|
||||
animate?: Record<string, unknown>;
|
||||
transition?: { duration?: number; delay?: number };
|
||||
}
|
||||
|
||||
export interface TextBlock {
|
||||
type: 'text';
|
||||
value: string;
|
||||
animation?: AnimationConfig;
|
||||
}
|
||||
|
||||
export interface PhotoBlock {
|
||||
|
|
@ -26,6 +36,7 @@ export interface PhotoBlock {
|
|||
alt: string;
|
||||
caption?: string;
|
||||
width?: ContainerWidth;
|
||||
animation?: AnimationConfig;
|
||||
}
|
||||
|
||||
export interface GraphicBlock {
|
||||
|
|
@ -34,6 +45,7 @@ export interface GraphicBlock {
|
|||
description?: string;
|
||||
notes?: string;
|
||||
width?: ContainerWidth;
|
||||
animation?: AnimationConfig;
|
||||
}
|
||||
|
||||
export type BodyBlock = TextBlock | PhotoBlock | GraphicBlock;
|
||||
|
|
@ -65,6 +77,38 @@ export interface StoryData extends StoryMeta {
|
|||
interface FreeformBlock {
|
||||
type: string;
|
||||
value: string | Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function parseAnimationConfig(raw: Record<string, unknown> | undefined): AnimationConfig | undefined {
|
||||
if (!raw) return undefined;
|
||||
const inView = raw.inView ?? raw['animation.inView'];
|
||||
if (inView === undefined && !raw.margin && !raw['animation.margin'] && !raw.initial && !raw.animate) {
|
||||
return undefined;
|
||||
}
|
||||
const margin = (raw.margin ?? raw['animation.margin']) as string | undefined;
|
||||
const amount = (raw.amount ?? raw['animation.amount']) as 'some' | 'all' | number | undefined;
|
||||
const initial = (raw.initial ?? raw['animation.initial']) as Record<string, unknown> | undefined;
|
||||
const animate = (raw.animate ?? raw['animation.animate']) as Record<string, unknown> | undefined;
|
||||
const transition = (raw.transition ?? raw['animation.transition']) as
|
||||
| { duration?: number; delay?: number }
|
||||
| undefined;
|
||||
return {
|
||||
inView: inView === true || inView === 'true',
|
||||
margin,
|
||||
amount,
|
||||
initial,
|
||||
animate,
|
||||
transition,
|
||||
};
|
||||
}
|
||||
|
||||
function getBlockAnimation(rawBlock: FreeformBlock): AnimationConfig | undefined {
|
||||
const nested = rawBlock.animation as Record<string, unknown> | undefined;
|
||||
if (nested && typeof nested === 'object') {
|
||||
return parseAnimationConfig(nested);
|
||||
}
|
||||
return parseAnimationConfig(rawBlock as unknown as Record<string, unknown>);
|
||||
}
|
||||
|
||||
function parseAuthors(raw: string | undefined): string[] {
|
||||
|
|
@ -87,9 +131,10 @@ function parseRaw(slug: string, raw: Record<string, unknown>): StoryData {
|
|||
const rawBody = (raw.body ?? []) as FreeformBlock[];
|
||||
|
||||
for (const block of rawBody) {
|
||||
const animation = getBlockAnimation(block);
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
body.push({ type: 'text', value: String(block.value ?? '') });
|
||||
body.push({ type: 'text', value: String(block.value ?? ''), animation });
|
||||
break;
|
||||
case 'photo': {
|
||||
const v = block.value as Record<string, string>;
|
||||
|
|
@ -99,6 +144,7 @@ function parseRaw(slug: string, raw: Record<string, unknown>): StoryData {
|
|||
alt: v.alt ?? '',
|
||||
caption: v.caption,
|
||||
width: (v.width as ContainerWidth) ?? undefined,
|
||||
animation,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
@ -110,6 +156,7 @@ function parseRaw(slug: string, raw: Record<string, unknown>): StoryData {
|
|||
description: v.description,
|
||||
notes: v.notes,
|
||||
width: (v.width as ContainerWidth) ?? undefined,
|
||||
animation,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
@ -156,7 +203,7 @@ export function listStories(): StoryMeta[] {
|
|||
const stories: StoryMeta[] = [];
|
||||
|
||||
for (const slug of slugs) {
|
||||
const amlPath = join(CONTENT_DIR, slug, 'story.aml');
|
||||
const amlPath = join(CONTENT_DIR, slug, `${slug}.aml`);
|
||||
if (!existsSync(amlPath)) continue;
|
||||
|
||||
const text = readFileSync(amlPath, 'utf-8');
|
||||
|
|
@ -182,7 +229,7 @@ export function listStories(): StoryMeta[] {
|
|||
}
|
||||
|
||||
export function loadStory(slug: string): StoryData | null {
|
||||
const amlPath = join(CONTENT_DIR, slug, 'story.aml');
|
||||
const amlPath = join(CONTENT_DIR, slug, `${slug}.aml`);
|
||||
if (!existsSync(amlPath)) return null;
|
||||
|
||||
const text = readFileSync(amlPath, 'utf-8');
|
||||
|
|
@ -193,6 +240,6 @@ export function loadStory(slug: string): StoryData | null {
|
|||
export function getAllSlugs(): string[] {
|
||||
if (!existsSync(CONTENT_DIR)) return [];
|
||||
return readdirSync(CONTENT_DIR, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && existsSync(join(CONTENT_DIR, d.name, 'story.aml')))
|
||||
.filter((d) => d.isDirectory() && existsSync(join(CONTENT_DIR, d.name, `${d.name}.aml`)))
|
||||
.map((d) => d.name);
|
||||
}
|
||||
|
|
|
|||
1
src/lib/cutoutStore.ts
Normal file
1
src/lib/cutoutStore.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const svgPathMap = new Map<string, string>();
|
||||
37
src/lib/locomotive.ts
Normal file
37
src/lib/locomotive.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Locomotive Scroll singleton for the app.
|
||||
*
|
||||
* Initialized once, re-scanned on page navigation so new `data-scroll`
|
||||
* elements are picked up. Smooth scrolling is disabled — we only use
|
||||
* Locomotive Scroll for viewport detection, progress tracking
|
||||
* (`data-scroll-css-progress`), and parallax (`data-scroll-speed`).
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let instance: InstanceType<typeof import('locomotive-scroll').default> | null =
|
||||
null;
|
||||
|
||||
export async function initScroll() {
|
||||
if (!browser) return null;
|
||||
destroyScroll();
|
||||
const { default: LocomotiveScroll } = await import('locomotive-scroll');
|
||||
instance = new LocomotiveScroll({
|
||||
lenisOptions: {
|
||||
smoothWheel: false,
|
||||
smoothTouch: false,
|
||||
},
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function destroyScroll() {
|
||||
if (instance) {
|
||||
instance.destroy();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getScroll() {
|
||||
return instance;
|
||||
}
|
||||
|
|
@ -1,13 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { initScroll, destroyScroll } from '$lib/lib/locomotive';
|
||||
import Theme from '$lib/components/Theme/Theme.svelte';
|
||||
import SiteHeader from '$lib/components/SiteHeader/SiteHeader.svelte';
|
||||
import SiteFooter from '$lib/components/SiteFooter/SiteFooter.svelte';
|
||||
import '$lib/scss/main.scss';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
initScroll();
|
||||
return destroyScroll;
|
||||
});
|
||||
|
||||
afterNavigate(async () => {
|
||||
await tick();
|
||||
initScroll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Theme base="light">
|
||||
<Theme base="dark">
|
||||
<SiteHeader />
|
||||
<div id="main-content" class="site-content">
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { loadStory, getAllSlugs } from '$lib/lib/content';
|
|||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad, EntryGenerator } from './$types';
|
||||
|
||||
export const prerender = 'auto';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const story = loadStory(params.slug);
|
||||
if (!story) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import FeaturePhoto from '$lib/components/FeaturePhoto/FeaturePhoto.svelte';
|
||||
import GraphicBlock from '$lib/components/GraphicBlock/GraphicBlock.svelte';
|
||||
import EndNotes from '$lib/components/EndNotes/EndNotes.svelte';
|
||||
import AnimatedBlock from '$lib/components/AnimatedBlock/AnimatedBlock.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const story = data.story;
|
||||
|
|
@ -48,23 +49,29 @@
|
|||
{/if}
|
||||
|
||||
{#each story.body as block}
|
||||
{#if block.type === 'text'}
|
||||
<BodyText text={block.value} />
|
||||
{:else if block.type === 'photo'}
|
||||
<FeaturePhoto
|
||||
src={block.src}
|
||||
altText={block.alt}
|
||||
caption={block.caption}
|
||||
width={block.width ?? 'normal'}
|
||||
/>
|
||||
{:else if block.type === 'graphic'}
|
||||
<GraphicBlock
|
||||
title={block.title}
|
||||
description={block.description}
|
||||
notes={block.notes}
|
||||
width={block.width ?? 'normal'}
|
||||
/>
|
||||
{/if}
|
||||
<AnimatedBlock animation={block.animation}>
|
||||
{#if block.type === 'text'}
|
||||
<BodyText text={block.value} />
|
||||
{:else if block.type === 'photo'}
|
||||
<FeaturePhoto
|
||||
src={block.src}
|
||||
altText={block.alt}
|
||||
caption={block.caption}
|
||||
width={block.width ?? 'normal'}
|
||||
/>
|
||||
{:else if block.type === 'graphic'}
|
||||
<GraphicBlock
|
||||
title={block.title}
|
||||
description={block.description}
|
||||
notes={block.notes}
|
||||
width={block.width ?? 'normal'}
|
||||
>
|
||||
{#snippet children()}
|
||||
<!-- Graphic content placeholder when block has no custom children -->
|
||||
{/snippet}
|
||||
</GraphicBlock>
|
||||
{/if}
|
||||
</AnimatedBlock>
|
||||
{/each}
|
||||
|
||||
{#if story.endNotes.length > 0}
|
||||
|
|
|
|||
52
src/routes/hello-world/+page.svelte
Normal file
52
src/routes/hello-world/+page.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script lang="ts">
|
||||
import ScrollScene from '$lib/components/ScrollScene/ScrollScene.svelte';
|
||||
import DollyZoomBackground from '$lib/components/Scroller/demo/components/dolly-zoom/DollyZoomBackground.svelte';
|
||||
|
||||
const manorImgSrc = '/stories/hello-world/manor.jpg';
|
||||
|
||||
// SVG silhouette clip-path from manort.svg (viewBox 2048×1367)
|
||||
const manorClipPath =
|
||||
'm0 428 7.326-11.25 3 5.75 3.75 7.75 6-4 3.25-7.75 6-8.25 5.75 4.5 1 5.75 2.75 6.75 4.5 8.25-3.25 3.25 3 9.75 4.25 9.25 3.25 2-2.25 4.5 1.75 8-4.25 1 .5 3.5v3.75l5-.25-.5 3.25v3.25l-2.5 5 2.5 4.25-1.75 3.75 3.5 6.75.5 5.25-3.75 4.25 2.75 6 2.75 6 3.75 5.75.75-9.5 3.25-6.75-2.75-4.25 2.25-3.75 2.25-4.5 1.75-17.25.25-10.75 10-21.75.25-7.25 14.25-16.5 7 21.5 2.75 13 5.5 4.5.5 11 4 3.25-.75 3-2.75 1.25 1.25 4 1.25 3.75-4 3 4.75 7.25-.25 6.5.5 6.5 2 2.75-3.75 2 1.25 10.5 3 11.75 5 21.25 2 11-1.25 11 5 4-1 8.25-.5 11-.5 12.75.75 29.5-3.25 15.25 3.75 5.5 2 6.5 2.5-3.5 21.75-60.75 6.5-3.25 86.25 6.5 165.5 11.5 106.25 2.5v-7.25l-7.75-1.5-8-2.75.25-4 17-29.75 2.5-19.5 5.25-21 2.25-8 .25-125-1.75-23.5 37.25-20.25 32.75 4.25-.5 21.25-3 2 .25 23.25 14.75 10.75-1 3.25-3 .5-.25 28.5 6 1.5 63 9 149 15.5 155.75 15.25 70.504 3.75-2-49.25-1.5-2.75 29-9.75 29.25 4.75-.25 3.25-.75 15-.25 40.25 3.5 3.25 9 4.25 97.5 12 84.5 9 12.75-.75-2-35.5-3-4.25-.5-10.25 38.75-10.75 19.25 3.25v17l-1.5 4.75 1.5 59 83.25 62.25 5 5.5-1 3.25-2 5.25-14 14.25-2.5 3 5.25 173 8.25 1.5 18-7 22.75 5.25 8.75 8 41.75 4.75 6.5 7.75 2.75 5.5 25.25-1 26.75 11 33.25 24.5 7.25 9.5 36.25-3.25 30.75-1.25 58.45 19.25 81.55 1 39-7 11.75-1.4-1-37.1s-9.25-6.25-8-15.75 7.25-13.25 8.5-11.5 7.5 6.5 8.25 9.5-4.25 17.25-3 14.5-1 6.25-1 6.25l1.25 33.75 7.9 3.6s26.7 3.5 41.7 4.4c15.4.924 25.17-12.982 48.15-11.5 31 2 53.17 9.5 53.17 9.5l.5 453H0l1.451-908.5L0 428Z';
|
||||
|
||||
let progress = $state(0);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Hello World — Dolly Zoom</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A scroll-driven dolly zoom animation demo using the Manor image."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="hello-world-page">
|
||||
<h1 class="visually-hidden">Hello World: Dolly Zoom Demo</h1>
|
||||
|
||||
<ScrollScene distance="300vh" bind:progress>
|
||||
<DollyZoomBackground
|
||||
imgSrc={manorImgSrc}
|
||||
fgClipPath={manorClipPath}
|
||||
clipWidth={2048}
|
||||
clipHeight={1367}
|
||||
{progress}
|
||||
/>
|
||||
</ScrollScene>
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
.hello-world-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
BIN
src/stories/hello-world/ai2html.ait
(Stored with Git LFS)
Normal file
BIN
src/stories/hello-world/ai2html.ait
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/stories/hello-world/dotcom.ait
(Stored with Git LFS)
Normal file
BIN
src/stories/hello-world/dotcom.ait
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/stories/hello-world/manor.ai
(Stored with Git LFS)
Normal file
BIN
src/stories/hello-world/manor.ai
(Stored with Git LFS)
Normal file
Binary file not shown.
48
src/stories/hello-world/manor.stories.svelte
Normal file
48
src/stories/hello-world/manor.stories.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Scroller from '$lib/components/Scroller/Scroller.svelte';
|
||||
import DollyZoomBackground from '$lib/components/Scroller/demo/components/dolly-zoom/DollyZoomBackground.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Hello World/Scroller Dolly Zoom',
|
||||
component: Scroller,
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const manorImgSrc = '/stories/hello-world/manor_xl.jpg';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
background: DollyZoomBackground,
|
||||
backgroundProps: { imgSrc: manorImgSrc },
|
||||
foreground: `#### Welcome to the Manor
|
||||
|
||||
Scroll down to experience the **Dolly Zoom** effect — a vertigo-like transformation inspired by [Alfred Hitchcock's technique](https://codepen.io/scharamoose/pen/PGpGez).
|
||||
|
||||
As you scroll, the image layers will shift in depth: the foreground zooms toward you while the background pulls away and softens.`,
|
||||
altText:
|
||||
'The Manor at Swan Harbour. A scroll-driven dolly zoom effect animates the image depth.',
|
||||
},
|
||||
{
|
||||
background: DollyZoomBackground,
|
||||
backgroundProps: { imgSrc: manorImgSrc },
|
||||
foreground: `#### Step 2 — The Effect Intensifies
|
||||
|
||||
Keep scrolling. The perspective distortion creates a disorienting, cinematic sensation.`,
|
||||
altText: 'The manor image with increased dolly zoom effect.',
|
||||
},
|
||||
{
|
||||
background: DollyZoomBackground,
|
||||
backgroundProps: { imgSrc: manorImgSrc },
|
||||
foreground: `#### Step 3 — Full Vertigo
|
||||
|
||||
At full scroll, the foreground pushes forward while the background blurs into the distance.`,
|
||||
altText: 'Maximum dolly zoom effect on the manor image.',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Story name="Manor Dolly Zoom" exportName="ManorDollyZoom">
|
||||
<Scroller steps={steps} foregroundPosition="middle" backgroundWidth="fluid" />
|
||||
</Story>
|
||||
BIN
src/stories/hello-world/sharecard.ait
(Stored with Git LFS)
Normal file
BIN
src/stories/hello-world/sharecard.ait
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -9,7 +9,7 @@ const config = {
|
|||
assets: 'build',
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true,
|
||||
strict: false,
|
||||
}),
|
||||
files: {
|
||||
lib: 'src',
|
||||
|
|
|
|||
Loading…
Reference in a new issue