Spaces:
Running
Running
thibaud frere
commited on
Commit
Β·
6b70860
1
Parent(s):
ad5db07
update doc, add some new components
Browse files- app/.astro/astro/content.d.ts +0 -227
- app/.astro/settings.json +1 -1
- app/astro.config.mjs +4 -2
- app/src/components/MultiImage.astro +342 -0
- app/src/components/Quote.astro +137 -0
- app/src/components/ResponsiveImage.astro +351 -112
- app/src/components/Sidenote.astro +56 -15
- app/src/content/article.mdx +9 -9
- app/src/content/assets/data/llm_benchmarks.json +2 -2
- app/src/content/chapters/{best-pratices.mdx β demo/best-pratices.mdx} +11 -13
- app/src/content/chapters/{components.mdx β demo/components.mdx} +137 -40
- app/src/content/chapters/{debug-components.mdx β demo/debug-components.mdx} +7 -7
- app/src/content/chapters/{getting-started.mdx β demo/getting-started.mdx} +6 -6
- app/src/content/chapters/{greetings.mdx β demo/greetings.mdx} +1 -1
- app/src/content/chapters/{introduction.mdx β demo/introduction.mdx} +11 -16
- app/src/content/chapters/{markdown.mdx β demo/markdown.mdx} +80 -21
- app/src/content/chapters/{vibe-coding-charts.mdx β demo/vibe-coding-charts.mdx} +5 -5
- app/src/content/chapters/{writing-your-content.mdx β demo/writing-your-content.mdx} +22 -22
- app/src/pages/index.astro +216 -84
- app/src/styles/_base.css +110 -11
- app/src/styles/_variables.css +16 -14
app/.astro/astro/content.d.ts
CHANGED
|
@@ -1,227 +0,0 @@
|
|
| 1 |
-
declare module 'astro:content' {
|
| 2 |
-
interface Render {
|
| 3 |
-
'.mdx': Promise<{
|
| 4 |
-
Content: import('astro').MarkdownInstance<{}>['Content'];
|
| 5 |
-
headings: import('astro').MarkdownHeading[];
|
| 6 |
-
remarkPluginFrontmatter: Record<string, any>;
|
| 7 |
-
components: import('astro').MDXInstance<{}>['components'];
|
| 8 |
-
}>;
|
| 9 |
-
}
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
declare module 'astro:content' {
|
| 13 |
-
interface RenderResult {
|
| 14 |
-
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
| 15 |
-
headings: import('astro').MarkdownHeading[];
|
| 16 |
-
remarkPluginFrontmatter: Record<string, any>;
|
| 17 |
-
}
|
| 18 |
-
interface Render {
|
| 19 |
-
'.md': Promise<RenderResult>;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export interface RenderedContent {
|
| 23 |
-
html: string;
|
| 24 |
-
metadata?: {
|
| 25 |
-
imagePaths: Array<string>;
|
| 26 |
-
[key: string]: unknown;
|
| 27 |
-
};
|
| 28 |
-
}
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
declare module 'astro:content' {
|
| 32 |
-
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
| 33 |
-
|
| 34 |
-
export type CollectionKey = keyof AnyEntryMap;
|
| 35 |
-
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
| 36 |
-
|
| 37 |
-
export type ContentCollectionKey = keyof ContentEntryMap;
|
| 38 |
-
export type DataCollectionKey = keyof DataEntryMap;
|
| 39 |
-
|
| 40 |
-
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
| 41 |
-
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
| 42 |
-
ContentEntryMap[C]
|
| 43 |
-
>['slug'];
|
| 44 |
-
|
| 45 |
-
/** @deprecated Use `getEntry` instead. */
|
| 46 |
-
export function getEntryBySlug<
|
| 47 |
-
C extends keyof ContentEntryMap,
|
| 48 |
-
E extends ValidContentEntrySlug<C> | (string & {}),
|
| 49 |
-
>(
|
| 50 |
-
collection: C,
|
| 51 |
-
// Note that this has to accept a regular string too, for SSR
|
| 52 |
-
entrySlug: E,
|
| 53 |
-
): E extends ValidContentEntrySlug<C>
|
| 54 |
-
? Promise<CollectionEntry<C>>
|
| 55 |
-
: Promise<CollectionEntry<C> | undefined>;
|
| 56 |
-
|
| 57 |
-
/** @deprecated Use `getEntry` instead. */
|
| 58 |
-
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
| 59 |
-
collection: C,
|
| 60 |
-
entryId: E,
|
| 61 |
-
): Promise<CollectionEntry<C>>;
|
| 62 |
-
|
| 63 |
-
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
| 64 |
-
collection: C,
|
| 65 |
-
filter?: (entry: CollectionEntry<C>) => entry is E,
|
| 66 |
-
): Promise<E[]>;
|
| 67 |
-
export function getCollection<C extends keyof AnyEntryMap>(
|
| 68 |
-
collection: C,
|
| 69 |
-
filter?: (entry: CollectionEntry<C>) => unknown,
|
| 70 |
-
): Promise<CollectionEntry<C>[]>;
|
| 71 |
-
|
| 72 |
-
export function getEntry<
|
| 73 |
-
C extends keyof ContentEntryMap,
|
| 74 |
-
E extends ValidContentEntrySlug<C> | (string & {}),
|
| 75 |
-
>(entry: {
|
| 76 |
-
collection: C;
|
| 77 |
-
slug: E;
|
| 78 |
-
}): E extends ValidContentEntrySlug<C>
|
| 79 |
-
? Promise<CollectionEntry<C>>
|
| 80 |
-
: Promise<CollectionEntry<C> | undefined>;
|
| 81 |
-
export function getEntry<
|
| 82 |
-
C extends keyof DataEntryMap,
|
| 83 |
-
E extends keyof DataEntryMap[C] | (string & {}),
|
| 84 |
-
>(entry: {
|
| 85 |
-
collection: C;
|
| 86 |
-
id: E;
|
| 87 |
-
}): E extends keyof DataEntryMap[C]
|
| 88 |
-
? Promise<DataEntryMap[C][E]>
|
| 89 |
-
: Promise<CollectionEntry<C> | undefined>;
|
| 90 |
-
export function getEntry<
|
| 91 |
-
C extends keyof ContentEntryMap,
|
| 92 |
-
E extends ValidContentEntrySlug<C> | (string & {}),
|
| 93 |
-
>(
|
| 94 |
-
collection: C,
|
| 95 |
-
slug: E,
|
| 96 |
-
): E extends ValidContentEntrySlug<C>
|
| 97 |
-
? Promise<CollectionEntry<C>>
|
| 98 |
-
: Promise<CollectionEntry<C> | undefined>;
|
| 99 |
-
export function getEntry<
|
| 100 |
-
C extends keyof DataEntryMap,
|
| 101 |
-
E extends keyof DataEntryMap[C] | (string & {}),
|
| 102 |
-
>(
|
| 103 |
-
collection: C,
|
| 104 |
-
id: E,
|
| 105 |
-
): E extends keyof DataEntryMap[C]
|
| 106 |
-
? Promise<DataEntryMap[C][E]>
|
| 107 |
-
: Promise<CollectionEntry<C> | undefined>;
|
| 108 |
-
|
| 109 |
-
/** Resolve an array of entry references from the same collection */
|
| 110 |
-
export function getEntries<C extends keyof ContentEntryMap>(
|
| 111 |
-
entries: {
|
| 112 |
-
collection: C;
|
| 113 |
-
slug: ValidContentEntrySlug<C>;
|
| 114 |
-
}[],
|
| 115 |
-
): Promise<CollectionEntry<C>[]>;
|
| 116 |
-
export function getEntries<C extends keyof DataEntryMap>(
|
| 117 |
-
entries: {
|
| 118 |
-
collection: C;
|
| 119 |
-
id: keyof DataEntryMap[C];
|
| 120 |
-
}[],
|
| 121 |
-
): Promise<CollectionEntry<C>[]>;
|
| 122 |
-
|
| 123 |
-
export function render<C extends keyof AnyEntryMap>(
|
| 124 |
-
entry: AnyEntryMap[C][string],
|
| 125 |
-
): Promise<RenderResult>;
|
| 126 |
-
|
| 127 |
-
export function reference<C extends keyof AnyEntryMap>(
|
| 128 |
-
collection: C,
|
| 129 |
-
): import('astro/zod').ZodEffects<
|
| 130 |
-
import('astro/zod').ZodString,
|
| 131 |
-
C extends keyof ContentEntryMap
|
| 132 |
-
? {
|
| 133 |
-
collection: C;
|
| 134 |
-
slug: ValidContentEntrySlug<C>;
|
| 135 |
-
}
|
| 136 |
-
: {
|
| 137 |
-
collection: C;
|
| 138 |
-
id: keyof DataEntryMap[C];
|
| 139 |
-
}
|
| 140 |
-
>;
|
| 141 |
-
// Allow generic `string` to avoid excessive type errors in the config
|
| 142 |
-
// if `dev` is not running to update as you edit.
|
| 143 |
-
// Invalid collection names will be caught at build time.
|
| 144 |
-
export function reference<C extends string>(
|
| 145 |
-
collection: C,
|
| 146 |
-
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
| 147 |
-
|
| 148 |
-
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
| 149 |
-
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
| 150 |
-
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
| 151 |
-
>;
|
| 152 |
-
|
| 153 |
-
type ContentEntryMap = {
|
| 154 |
-
"chapters": {
|
| 155 |
-
"01_introduction.mdx": {
|
| 156 |
-
id: "01_introduction.mdx";
|
| 157 |
-
slug: "01_introduction";
|
| 158 |
-
body: string;
|
| 159 |
-
collection: "chapters";
|
| 160 |
-
data: any
|
| 161 |
-
} & { render(): Render[".mdx"] };
|
| 162 |
-
"02_classic_robotics.mdx": {
|
| 163 |
-
id: "02_classic_robotics.mdx";
|
| 164 |
-
slug: "02_classic_robotics";
|
| 165 |
-
body: string;
|
| 166 |
-
collection: "chapters";
|
| 167 |
-
data: any
|
| 168 |
-
} & { render(): Render[".mdx"] };
|
| 169 |
-
"03_reinforcement_learning.mdx": {
|
| 170 |
-
id: "03_reinforcement_learning.mdx";
|
| 171 |
-
slug: "03_reinforcement_learning";
|
| 172 |
-
body: string;
|
| 173 |
-
collection: "chapters";
|
| 174 |
-
data: any
|
| 175 |
-
} & { render(): Render[".mdx"] };
|
| 176 |
-
"04_imitation_learning.mdx": {
|
| 177 |
-
id: "04_imitation_learning.mdx";
|
| 178 |
-
slug: "04_imitation_learning";
|
| 179 |
-
body: string;
|
| 180 |
-
collection: "chapters";
|
| 181 |
-
data: any
|
| 182 |
-
} & { render(): Render[".mdx"] };
|
| 183 |
-
"05_foundation_models.mdx": {
|
| 184 |
-
id: "05_foundation_models.mdx";
|
| 185 |
-
slug: "05_foundation_models";
|
| 186 |
-
body: string;
|
| 187 |
-
collection: "chapters";
|
| 188 |
-
data: any
|
| 189 |
-
} & { render(): Render[".mdx"] };
|
| 190 |
-
"06_next_directions.mdx": {
|
| 191 |
-
id: "06_next_directions.mdx";
|
| 192 |
-
slug: "06_next_directions";
|
| 193 |
-
body: string;
|
| 194 |
-
collection: "chapters";
|
| 195 |
-
data: any
|
| 196 |
-
} & { render(): Render[".mdx"] };
|
| 197 |
-
"07_conclusions.mdx": {
|
| 198 |
-
id: "07_conclusions.mdx";
|
| 199 |
-
slug: "07_conclusions";
|
| 200 |
-
body: string;
|
| 201 |
-
collection: "chapters";
|
| 202 |
-
data: any
|
| 203 |
-
} & { render(): Render[".mdx"] };
|
| 204 |
-
"A_foreword.mdx": {
|
| 205 |
-
id: "A_foreword.mdx";
|
| 206 |
-
slug: "a_foreword";
|
| 207 |
-
body: string;
|
| 208 |
-
collection: "chapters";
|
| 209 |
-
data: any
|
| 210 |
-
} & { render(): Render[".mdx"] };
|
| 211 |
-
};
|
| 212 |
-
|
| 213 |
-
};
|
| 214 |
-
|
| 215 |
-
type DataEntryMap = {
|
| 216 |
-
"assets": Record<string, {
|
| 217 |
-
id: string;
|
| 218 |
-
collection: "assets";
|
| 219 |
-
data: any;
|
| 220 |
-
}>;
|
| 221 |
-
|
| 222 |
-
};
|
| 223 |
-
|
| 224 |
-
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
| 225 |
-
|
| 226 |
-
export type ContentConfig = never;
|
| 227 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/.astro/settings.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 58
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5cd907e18678e90f342986642cb5fcdca79f1c4a61fe1d75caa2df036e69070e
|
| 3 |
size 58
|
app/astro.config.mjs
CHANGED
|
@@ -56,11 +56,13 @@ export default defineConfig({
|
|
| 56 |
rehypePlugins: [
|
| 57 |
rehypeSlug,
|
| 58 |
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
|
| 59 |
-
rehypeKatex,
|
|
|
|
|
|
|
| 60 |
[rehypeCitation, {
|
| 61 |
bibliography: 'src/content/bibliography.bib',
|
| 62 |
linkCitations: true,
|
| 63 |
-
csl: "apa"
|
| 64 |
}],
|
| 65 |
rehypeReferencesAndFootnotes,
|
| 66 |
rehypeRestoreAtInCode,
|
|
|
|
| 56 |
rehypePlugins: [
|
| 57 |
rehypeSlug,
|
| 58 |
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
|
| 59 |
+
[rehypeKatex, {
|
| 60 |
+
trust: true,
|
| 61 |
+
}],
|
| 62 |
[rehypeCitation, {
|
| 63 |
bibliography: 'src/content/bibliography.bib',
|
| 64 |
linkCitations: true,
|
| 65 |
+
csl: "apa",
|
| 66 |
}],
|
| 67 |
rehypeReferencesAndFootnotes,
|
| 68 |
rehypeRestoreAtInCode,
|
app/src/components/MultiImage.astro
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
// @ts-ignore - types provided by Astro at runtime
|
| 3 |
+
import ResponsiveImage from "./ResponsiveImage.astro";
|
| 4 |
+
|
| 5 |
+
interface ImageItem {
|
| 6 |
+
/** Source image imported via astro:assets */
|
| 7 |
+
src: any;
|
| 8 |
+
/** Alt text for accessibility */
|
| 9 |
+
alt: string;
|
| 10 |
+
/** Individual caption for this image */
|
| 11 |
+
caption?: string;
|
| 12 |
+
/** Optional individual image ID for referencing */
|
| 13 |
+
id?: string;
|
| 14 |
+
/** Enable zoom on this specific image (defaults to parent zoomable setting) */
|
| 15 |
+
zoomable?: boolean;
|
| 16 |
+
/** Enable download on this specific image (defaults to parent downloadable setting) */
|
| 17 |
+
downloadable?: boolean;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
interface Props {
|
| 21 |
+
/** Array of images to display */
|
| 22 |
+
images: ImageItem[];
|
| 23 |
+
/** Global caption for the entire figure */
|
| 24 |
+
caption?: string;
|
| 25 |
+
/** Layout mode: number of columns or 'auto' for responsive */
|
| 26 |
+
layout?: "2-column" | "3-column" | "4-column" | "auto";
|
| 27 |
+
/** Enable medium-zoom behavior on all images (can be overridden per image) */
|
| 28 |
+
zoomable?: boolean;
|
| 29 |
+
/** Show download buttons on all images (can be overridden per image) */
|
| 30 |
+
downloadable?: boolean;
|
| 31 |
+
/** Optional class to apply on the wrapper */
|
| 32 |
+
class?: string;
|
| 33 |
+
/** Optional global ID for the multi-image figure */
|
| 34 |
+
id?: string;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const {
|
| 38 |
+
images,
|
| 39 |
+
caption,
|
| 40 |
+
layout = "3-column",
|
| 41 |
+
zoomable = false,
|
| 42 |
+
downloadable = false,
|
| 43 |
+
class: className,
|
| 44 |
+
id,
|
| 45 |
+
} = Astro.props as Props;
|
| 46 |
+
|
| 47 |
+
const hasCaptionSlot = Astro.slots.has("caption");
|
| 48 |
+
const hasCaption =
|
| 49 |
+
hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
|
| 50 |
+
const uid = `mi_${Math.random().toString(36).slice(2)}`;
|
| 51 |
+
|
| 52 |
+
// Generate CSS grid columns based on layout
|
| 53 |
+
const getGridColumns = () => {
|
| 54 |
+
switch (layout) {
|
| 55 |
+
case "2-column":
|
| 56 |
+
return "repeat(2, 1fr)";
|
| 57 |
+
case "3-column":
|
| 58 |
+
return "repeat(3, 1fr)";
|
| 59 |
+
case "4-column":
|
| 60 |
+
return "repeat(4, 1fr)";
|
| 61 |
+
case "auto":
|
| 62 |
+
return "repeat(auto-fit, minmax(200px, 1fr))";
|
| 63 |
+
default:
|
| 64 |
+
return "repeat(3, 1fr)";
|
| 65 |
+
}
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const gridColumns = getGridColumns();
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
<div
|
| 72 |
+
class={`multi-image ${className || ""}`}
|
| 73 |
+
data-mi-root={uid}
|
| 74 |
+
data-layout={layout}
|
| 75 |
+
{id}
|
| 76 |
+
>
|
| 77 |
+
{
|
| 78 |
+
hasCaption ? (
|
| 79 |
+
<figure class="multi-image-figure">
|
| 80 |
+
<div
|
| 81 |
+
class="multi-image-grid"
|
| 82 |
+
style={`grid-template-columns: ${gridColumns}`}
|
| 83 |
+
>
|
| 84 |
+
{images.map((image, index) => (
|
| 85 |
+
<div class="multi-image-item">
|
| 86 |
+
<ResponsiveImage
|
| 87 |
+
src={image.src}
|
| 88 |
+
alt={image.alt}
|
| 89 |
+
zoomable={image.zoomable ?? zoomable}
|
| 90 |
+
downloadable={
|
| 91 |
+
image.downloadable ?? downloadable
|
| 92 |
+
}
|
| 93 |
+
class="multi-image-img"
|
| 94 |
+
/>
|
| 95 |
+
{image.caption && (
|
| 96 |
+
<div class="multi-image-subcaption">
|
| 97 |
+
{image.caption}
|
| 98 |
+
</div>
|
| 99 |
+
)}
|
| 100 |
+
{image.id && (
|
| 101 |
+
<span
|
| 102 |
+
id={image.id}
|
| 103 |
+
style="position: absolute;"
|
| 104 |
+
/>
|
| 105 |
+
)}
|
| 106 |
+
</div>
|
| 107 |
+
))}
|
| 108 |
+
</div>
|
| 109 |
+
<figcaption class="multi-image-caption">
|
| 110 |
+
{hasCaptionSlot ? (
|
| 111 |
+
<slot name="caption" />
|
| 112 |
+
) : (
|
| 113 |
+
caption && <span set:html={caption} />
|
| 114 |
+
)}
|
| 115 |
+
</figcaption>
|
| 116 |
+
</figure>
|
| 117 |
+
) : (
|
| 118 |
+
<div
|
| 119 |
+
class="multi-image-grid"
|
| 120 |
+
style={`grid-template-columns: ${gridColumns}`}
|
| 121 |
+
>
|
| 122 |
+
{images.map((image, index) => (
|
| 123 |
+
<div class="multi-image-item">
|
| 124 |
+
<ResponsiveImage
|
| 125 |
+
src={image.src}
|
| 126 |
+
alt={image.alt}
|
| 127 |
+
zoomable={image.zoomable ?? zoomable}
|
| 128 |
+
downloadable={image.downloadable ?? downloadable}
|
| 129 |
+
class="multi-image-img"
|
| 130 |
+
/>
|
| 131 |
+
{image.caption && (
|
| 132 |
+
<div class="multi-image-subcaption">
|
| 133 |
+
{image.caption}
|
| 134 |
+
</div>
|
| 135 |
+
)}
|
| 136 |
+
{image.id && (
|
| 137 |
+
<span id={image.id} style="position: absolute;" />
|
| 138 |
+
)}
|
| 139 |
+
</div>
|
| 140 |
+
))}
|
| 141 |
+
</div>
|
| 142 |
+
)
|
| 143 |
+
}
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<style>
|
| 147 |
+
.multi-image {
|
| 148 |
+
margin: var(--block-spacing-y) 0;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.multi-image-figure {
|
| 152 |
+
margin: 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.multi-image-grid {
|
| 156 |
+
display: grid;
|
| 157 |
+
gap: 1rem;
|
| 158 |
+
align-items: start;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.multi-image-item {
|
| 162 |
+
display: flex;
|
| 163 |
+
flex-direction: column;
|
| 164 |
+
text-align: center;
|
| 165 |
+
position: relative;
|
| 166 |
+
z-index: var(--z-content);
|
| 167 |
+
transition: z-index 0.3s ease;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Quand medium-zoom est actif, masquer temporairement les autres images du multi-image */
|
| 171 |
+
:global(.medium-zoom--opened) .multi-image-item {
|
| 172 |
+
opacity: 0;
|
| 173 |
+
z-index: calc(var(--z-base) - 1);
|
| 174 |
+
transition:
|
| 175 |
+
opacity 0.3s ease,
|
| 176 |
+
z-index 0.3s ease;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/* Masquer spΓ©cifiquement les captions pendant le zoom - approche radicale */
|
| 180 |
+
:global(.medium-zoom--opened) .multi-image-subcaption {
|
| 181 |
+
display: none !important;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/* Masquer complètement tous les éléments multi-image pendant le zoom */
|
| 185 |
+
:global(.medium-zoom--opened) .multi-image {
|
| 186 |
+
z-index: -1 !important;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* Masquer tous les textes de l'ensemble des multi-images */
|
| 190 |
+
:global(.medium-zoom--opened) .multi-image-caption {
|
| 191 |
+
display: none !important;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/* L'image actuellement zoomΓ©e reste visible */
|
| 195 |
+
:global(.medium-zoom--opened)
|
| 196 |
+
.multi-image-item:has(:global(.medium-zoom--opened)) {
|
| 197 |
+
opacity: 1;
|
| 198 |
+
z-index: var(--z-overlay);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/* Fallback pour navigateurs sans support :has() */
|
| 202 |
+
:global(.medium-zoom--opened) .multi-image-item.zoom-active {
|
| 203 |
+
opacity: 1 !important;
|
| 204 |
+
z-index: var(--z-overlay) !important;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* Garder la caption de l'image active visible */
|
| 208 |
+
:global(.medium-zoom--opened)
|
| 209 |
+
.multi-image-item.zoom-active
|
| 210 |
+
.multi-image-subcaption {
|
| 211 |
+
opacity: 1 !important;
|
| 212 |
+
z-index: var(--z-overlay) !important;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.multi-image-item :global(.ri-root) {
|
| 216 |
+
margin: 0;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.multi-image-item :global(figure) {
|
| 220 |
+
margin: 0;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.multi-image-img {
|
| 224 |
+
width: 100%;
|
| 225 |
+
height: auto;
|
| 226 |
+
object-fit: contain;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.multi-image-subcaption {
|
| 230 |
+
font-size: 0.85rem;
|
| 231 |
+
color: var(--muted-color);
|
| 232 |
+
margin-top: 0.5rem;
|
| 233 |
+
line-height: 1.4;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.multi-image-caption {
|
| 237 |
+
text-align: left;
|
| 238 |
+
font-size: 0.9rem;
|
| 239 |
+
color: var(--muted-color);
|
| 240 |
+
margin-top: 1rem;
|
| 241 |
+
line-height: 1.4;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/* Responsive behavior */
|
| 245 |
+
@media (max-width: 768px) {
|
| 246 |
+
.multi-image-grid[style*="repeat(3, 1fr)"],
|
| 247 |
+
.multi-image-grid[style*="repeat(4, 1fr)"] {
|
| 248 |
+
grid-template-columns: 1fr !important;
|
| 249 |
+
gap: 1.5rem;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.multi-image-grid[style*="repeat(2, 1fr)"] {
|
| 253 |
+
grid-template-columns: 1fr !important;
|
| 254 |
+
gap: 1.5rem;
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
@media (min-width: 769px) and (max-width: 1024px) {
|
| 259 |
+
.multi-image-grid[style*="repeat(4, 1fr)"] {
|
| 260 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* Equal height images when desired */
|
| 265 |
+
.multi-image[data-layout*="column"] .multi-image-item :global(img) {
|
| 266 |
+
height: 200px;
|
| 267 |
+
object-fit: contain;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
/* Auto layout gets flexible heights */
|
| 271 |
+
.multi-image[data-layout="auto"] .multi-image-item :global(img) {
|
| 272 |
+
height: auto;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/* Ensure images maintain aspect ratio */
|
| 276 |
+
.multi-image-item :global(img) {
|
| 277 |
+
max-width: 100%;
|
| 278 |
+
display: block;
|
| 279 |
+
margin: 0 auto;
|
| 280 |
+
}
|
| 281 |
+
</style>
|
| 282 |
+
|
| 283 |
+
<script>
|
| 284 |
+
// Enhanced medium-zoom integration for MultiImage
|
| 285 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 286 |
+
// AmΓ©lioration du comportement des MultiImage avec medium-zoom
|
| 287 |
+
const multiImages = document.querySelectorAll(".multi-image");
|
| 288 |
+
|
| 289 |
+
multiImages.forEach((multiImage) => {
|
| 290 |
+
const items = multiImage.querySelectorAll(".multi-image-item");
|
| 291 |
+
const zoomableImages = multiImage.querySelectorAll(
|
| 292 |
+
'img[data-zoomable="1"]',
|
| 293 |
+
);
|
| 294 |
+
|
| 295 |
+
zoomableImages.forEach((img) => {
|
| 296 |
+
img.addEventListener("click", () => {
|
| 297 |
+
// Trouver l'item parent de l'image cliquΓ©e et le ri-root
|
| 298 |
+
const activeItem = img.closest(".multi-image-item");
|
| 299 |
+
const riRoot = img.closest(".ri-root");
|
| 300 |
+
|
| 301 |
+
// Nettoyer TOUS les zoom-active (MultiImage items et ResponsiveImage)
|
| 302 |
+
document
|
| 303 |
+
.querySelectorAll(
|
| 304 |
+
".multi-image-item.zoom-active, .ri-root.zoom-active",
|
| 305 |
+
)
|
| 306 |
+
.forEach((el) => el.classList.remove("zoom-active"));
|
| 307 |
+
|
| 308 |
+
// Ajouter zoom-active aux Γ©lΓ©ments actifs
|
| 309 |
+
if (activeItem) {
|
| 310 |
+
activeItem.classList.add("zoom-active");
|
| 311 |
+
}
|
| 312 |
+
if (riRoot) {
|
| 313 |
+
riRoot.classList.add("zoom-active");
|
| 314 |
+
}
|
| 315 |
+
});
|
| 316 |
+
});
|
| 317 |
+
});
|
| 318 |
+
|
| 319 |
+
// Nettoyer TOUTES les classes lors de la fermeture du zoom
|
| 320 |
+
document.addEventListener("click", (e) => {
|
| 321 |
+
if (e.target.classList.contains("medium-zoom-overlay")) {
|
| 322 |
+
// Zoom fermΓ©, nettoyer toutes les classes zoom-active
|
| 323 |
+
document
|
| 324 |
+
.querySelectorAll(
|
| 325 |
+
".multi-image-item.zoom-active, .ri-root.zoom-active",
|
| 326 |
+
)
|
| 327 |
+
.forEach((item) => item.classList.remove("zoom-active"));
|
| 328 |
+
}
|
| 329 |
+
});
|
| 330 |
+
|
| 331 |
+
// Γcouter les Γ©vΓ©nements clavier pour fermer le zoom
|
| 332 |
+
document.addEventListener("keydown", (e) => {
|
| 333 |
+
if (e.key === "Escape") {
|
| 334 |
+
document
|
| 335 |
+
.querySelectorAll(
|
| 336 |
+
".multi-image-item.zoom-active, .ri-root.zoom-active",
|
| 337 |
+
)
|
| 338 |
+
.forEach((item) => item.classList.remove("zoom-active"));
|
| 339 |
+
}
|
| 340 |
+
});
|
| 341 |
+
});
|
| 342 |
+
</script>
|
app/src/components/Quote.astro
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
interface Props {
|
| 3 |
+
author?: string;
|
| 4 |
+
source?: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
const { author, source } = Astro.props;
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
<blockquote class="quote">
|
| 11 |
+
<div class="quote__text">
|
| 12 |
+
<slot />
|
| 13 |
+
</div>
|
| 14 |
+
{
|
| 15 |
+
(author || source) && (
|
| 16 |
+
<footer class="quote__footer">
|
| 17 |
+
{author && <cite class="quote__author">{author}</cite>}
|
| 18 |
+
{source && <span class="quote__source">{source}</span>}
|
| 19 |
+
</footer>
|
| 20 |
+
)
|
| 21 |
+
}
|
| 22 |
+
</blockquote>
|
| 23 |
+
|
| 24 |
+
<style>
|
| 25 |
+
.quote {
|
| 26 |
+
font-family: var(--default-font-family);
|
| 27 |
+
margin: 32px 0;
|
| 28 |
+
max-width: 600px;
|
| 29 |
+
text-align: left;
|
| 30 |
+
position: relative;
|
| 31 |
+
padding: 0;
|
| 32 |
+
border: none;
|
| 33 |
+
background: none;
|
| 34 |
+
box-shadow: none;
|
| 35 |
+
white-space: normal;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.quote::before {
|
| 39 |
+
content: '"';
|
| 40 |
+
position: absolute;
|
| 41 |
+
top: -24px;
|
| 42 |
+
left: -30px;
|
| 43 |
+
font-size: 8rem;
|
| 44 |
+
font-family: var(--default-font-family);
|
| 45 |
+
font-weight: 400;
|
| 46 |
+
color: var(--text-color);
|
| 47 |
+
opacity: 0.05;
|
| 48 |
+
z-index: -1;
|
| 49 |
+
line-height: 1;
|
| 50 |
+
pointer-events: none;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.quote__text {
|
| 54 |
+
font-size: 1.5rem;
|
| 55 |
+
line-height: 1.4;
|
| 56 |
+
font-style: normal;
|
| 57 |
+
font-weight: 400;
|
| 58 |
+
font-family:
|
| 59 |
+
"SF Pro Text",
|
| 60 |
+
-apple-system,
|
| 61 |
+
BlinkMacSystemFont,
|
| 62 |
+
"Segoe UI",
|
| 63 |
+
sans-serif;
|
| 64 |
+
color: var(--text-color);
|
| 65 |
+
margin-bottom: 12px;
|
| 66 |
+
padding: 0;
|
| 67 |
+
letter-spacing: -0.01em;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.quote__text p {
|
| 71 |
+
margin: 0;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.quote__footer {
|
| 75 |
+
font-size: 0.875rem;
|
| 76 |
+
color: var(--muted-color);
|
| 77 |
+
display: flex;
|
| 78 |
+
justify-content: flex-start;
|
| 79 |
+
align-items: center;
|
| 80 |
+
gap: 6px;
|
| 81 |
+
margin-top: 0;
|
| 82 |
+
font-family:
|
| 83 |
+
"SF Pro Text",
|
| 84 |
+
-apple-system,
|
| 85 |
+
BlinkMacSystemFont,
|
| 86 |
+
"Segoe UI",
|
| 87 |
+
sans-serif;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.quote__author {
|
| 91 |
+
font-weight: 500;
|
| 92 |
+
font-style: normal;
|
| 93 |
+
color: var(--text-color);
|
| 94 |
+
opacity: 0.85;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.quote__author::before {
|
| 98 |
+
content: "β ";
|
| 99 |
+
opacity: 0.6;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.quote__source {
|
| 103 |
+
font-weight: 400;
|
| 104 |
+
opacity: 0.6;
|
| 105 |
+
font-style: italic;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.quote__source::before {
|
| 109 |
+
content: "β ";
|
| 110 |
+
font-style: normal;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
@media (max-width: 640px) {
|
| 114 |
+
.quote {
|
| 115 |
+
margin: 24px 0;
|
| 116 |
+
max-width: 100%;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.quote::before {
|
| 120 |
+
font-size: 6rem;
|
| 121 |
+
top: -18px;
|
| 122 |
+
left: -16px;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.quote__text {
|
| 126 |
+
font-size: 1.25rem;
|
| 127 |
+
line-height: 1.45;
|
| 128 |
+
padding: 0;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.quote__footer {
|
| 132 |
+
flex-direction: row;
|
| 133 |
+
gap: 6px;
|
| 134 |
+
font-size: 0.8rem;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
</style>
|
app/src/components/ResponsiveImage.astro
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
// @ts-ignore - types provided by Astro at runtime
|
| 3 |
-
import { Image } from
|
| 4 |
|
| 5 |
interface Props {
|
| 6 |
/** Source image imported via astro:assets */
|
|
@@ -29,82 +29,172 @@ interface Props {
|
|
| 29 |
[key: string]: any;
|
| 30 |
}
|
| 31 |
|
| 32 |
-
const {
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
const uid = `ri_${Math.random().toString(36).slice(2)}`;
|
| 36 |
-
const dataZoomable =
|
| 37 |
-
|
| 38 |
-
const
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
---
|
|
|
|
| 42 |
<div class="ri-root" data-ri-root={uid}>
|
| 43 |
-
{
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
<Image {...imgProps} data-zoomable={dataZoomable} />
|
| 62 |
</a>
|
| 63 |
) : (
|
| 64 |
<Image {...imgProps} data-zoomable={dataZoomable} />
|
| 65 |
-
)
|
| 66 |
-
)}
|
| 67 |
-
<figcaption>
|
| 68 |
-
{hasCaptionSlot ? (
|
| 69 |
-
<slot name="caption" />
|
| 70 |
-
) : (
|
| 71 |
-
caption && <span set:html={caption} />
|
| 72 |
)}
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
<span class="img-dl-wrap">
|
| 78 |
{hasLink ? (
|
| 79 |
-
<a
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</a>
|
| 82 |
) : (
|
| 83 |
-
<Image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
)}
|
| 85 |
-
<button
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
</button>
|
| 88 |
</span>
|
| 89 |
-
) : (
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
<Image {...imgProps} data-zoomable={dataZoomable} />
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
)
|
| 98 |
-
|
| 99 |
</div>
|
| 100 |
|
| 101 |
<script is:inline>
|
| 102 |
(() => {
|
|
|
|
| 103 |
const scriptEl = document.currentScript;
|
|
|
|
| 104 |
const root = scriptEl ? scriptEl.previousElementSibling : null;
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
// medium-zoom integration scoped to this image only
|
| 110 |
const ensureMediumZoomReady = (cb) => {
|
|
@@ -112,99 +202,174 @@ const resolvedRel = hasLink ? (linkRel || 'noopener noreferrer') : undefined;
|
|
| 112 |
if (window.mediumZoom) return cb();
|
| 113 |
const retry = () => {
|
| 114 |
// @ts-ignore
|
| 115 |
-
if (window.mediumZoom) cb();
|
|
|
|
| 116 |
};
|
| 117 |
retry();
|
| 118 |
};
|
| 119 |
|
| 120 |
const initZoomIfNeeded = () => {
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
ensureMediumZoomReady(() => {
|
| 125 |
// @ts-ignore
|
| 126 |
-
const instance = window.mediumZoom
|
|
|
|
|
|
|
|
|
|
| 127 |
if (!instance) return;
|
| 128 |
let onScrollLike;
|
| 129 |
const attachCloseOnScroll = () => {
|
| 130 |
if (onScrollLike) return;
|
| 131 |
-
onScrollLike = () => {
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
};
|
| 136 |
const detachCloseOnScroll = () => {
|
| 137 |
if (!onScrollLike) return;
|
| 138 |
-
window.removeEventListener(
|
| 139 |
-
window.removeEventListener(
|
| 140 |
-
window.removeEventListener(
|
| 141 |
onScrollLike = null;
|
| 142 |
};
|
| 143 |
-
try {
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
const themeObserver = new MutationObserver(() => {
|
| 146 |
-
const dark =
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
});
|
| 149 |
-
themeObserver.observe(document.documentElement, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
});
|
| 151 |
};
|
| 152 |
|
| 153 |
// Download button handler
|
| 154 |
-
const dlBtn =
|
| 155 |
if (dlBtn) {
|
| 156 |
-
dlBtn.addEventListener(
|
| 157 |
try {
|
| 158 |
ev.preventDefault();
|
| 159 |
ev.stopPropagation();
|
| 160 |
const pickHrefAndName = () => {
|
| 161 |
-
const current = img.currentSrc || img.src ||
|
| 162 |
-
let href = img.getAttribute(
|
| 163 |
const deriveName = () => {
|
| 164 |
try {
|
| 165 |
const u = new URL(current, location.href);
|
| 166 |
-
const rawHref = u.searchParams.get(
|
| 167 |
-
const candidate = rawHref
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
if (m && m[1]) return m[1];
|
| 172 |
-
return base ||
|
| 173 |
-
} catch {
|
|
|
|
|
|
|
| 174 |
};
|
| 175 |
-
const name = img.getAttribute(
|
| 176 |
return { href, name };
|
| 177 |
};
|
| 178 |
const picked = pickHrefAndName();
|
| 179 |
-
const res = await fetch(picked.href, { credentials:
|
| 180 |
const blob = await res.blob();
|
| 181 |
const objectUrl = URL.createObjectURL(blob);
|
| 182 |
-
const tmp = document.createElement(
|
| 183 |
tmp.href = objectUrl;
|
| 184 |
-
tmp.download = picked.name ||
|
| 185 |
-
tmp.target =
|
| 186 |
-
tmp.rel =
|
| 187 |
-
tmp.style.display =
|
| 188 |
document.body.appendChild(tmp);
|
| 189 |
tmp.click();
|
| 190 |
-
setTimeout(() => {
|
|
|
|
|
|
|
|
|
|
| 191 |
} catch {}
|
| 192 |
});
|
| 193 |
}
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
| 197 |
})();
|
| 198 |
</script>
|
| 199 |
|
| 200 |
-
|
| 201 |
<style>
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
figcaption {
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
/* Zoomable overlay container (if used by any lightbox implementation) */
|
| 210 |
[data-zoom-overlay],
|
|
@@ -215,16 +380,30 @@ const resolvedRel = hasLink ? (linkRel || 'noopener noreferrer') : undefined;
|
|
| 215 |
}
|
| 216 |
|
| 217 |
/* Download link inside figures */
|
| 218 |
-
figure .download-link {
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
-
|
| 221 |
-
img[data-zoomable] {
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
/* Download button for img[data-downloadable] */
|
| 225 |
-
figure.has-dl-btn {
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
.img-dl-btn {
|
| 229 |
position: absolute;
|
| 230 |
right: 8px;
|
|
@@ -236,12 +415,72 @@ const resolvedRel = hasLink ? (linkRel || 'noopener noreferrer') : undefined;
|
|
| 236 |
border-radius: 6px;
|
| 237 |
color: white;
|
| 238 |
text-decoration: none;
|
| 239 |
-
border: 1px solid rgba(255,255,255,0.25);
|
| 240 |
z-index: var(--z-elevated);
|
| 241 |
display: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
-
|
| 244 |
-
.img-dl-wrap:hover .img-dl-btn { display: inline-flex; }
|
| 245 |
-
[data-theme="dark"] .img-dl-btn { background: rgba(255,255,255,0.15); color: white; border-color: rgba(255,255,255,0.25); }
|
| 246 |
-
[data-theme="dark"] .img-dl-btn:hover { background: rgba(255,255,255,0.25); }
|
| 247 |
-
</style>
|
|
|
|
| 1 |
---
|
| 2 |
// @ts-ignore - types provided by Astro at runtime
|
| 3 |
+
import { Image } from "astro:assets";
|
| 4 |
|
| 5 |
interface Props {
|
| 6 |
/** Source image imported via astro:assets */
|
|
|
|
| 29 |
[key: string]: any;
|
| 30 |
}
|
| 31 |
|
| 32 |
+
const {
|
| 33 |
+
caption,
|
| 34 |
+
figureClass,
|
| 35 |
+
zoomable,
|
| 36 |
+
downloadable,
|
| 37 |
+
downloadName,
|
| 38 |
+
downloadSrc,
|
| 39 |
+
linkHref,
|
| 40 |
+
linkTarget,
|
| 41 |
+
linkRel,
|
| 42 |
+
...imgProps
|
| 43 |
+
} = Astro.props as Props;
|
| 44 |
+
const hasCaptionSlot = Astro.slots.has("caption");
|
| 45 |
+
const hasCaption =
|
| 46 |
+
hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
|
| 47 |
const uid = `ri_${Math.random().toString(36).slice(2)}`;
|
| 48 |
+
const dataZoomable =
|
| 49 |
+
zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined;
|
| 50 |
+
const dataDownloadable =
|
| 51 |
+
downloadable === true || (imgProps as any)["data-downloadable"]
|
| 52 |
+
? "1"
|
| 53 |
+
: undefined;
|
| 54 |
+
const hasLink = typeof linkHref === "string" && linkHref.length > 0;
|
| 55 |
+
const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
|
| 56 |
+
const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
| 57 |
---
|
| 58 |
+
|
| 59 |
<div class="ri-root" data-ri-root={uid}>
|
| 60 |
+
{
|
| 61 |
+
hasCaption ? (
|
| 62 |
+
<figure
|
| 63 |
+
class={(figureClass || "") + (dataDownloadable ? " has-dl-btn" : "")}
|
| 64 |
+
>
|
| 65 |
+
{dataDownloadable ? (
|
| 66 |
+
<span class="img-dl-wrap">
|
| 67 |
+
{hasLink ? (
|
| 68 |
+
<a
|
| 69 |
+
class="ri-link"
|
| 70 |
+
href={linkHref}
|
| 71 |
+
target={resolvedTarget}
|
| 72 |
+
rel={resolvedRel}
|
| 73 |
+
>
|
| 74 |
+
<Image
|
| 75 |
+
{...imgProps}
|
| 76 |
+
data-zoomable={dataZoomable}
|
| 77 |
+
data-downloadable={dataDownloadable}
|
| 78 |
+
data-download-name={downloadName}
|
| 79 |
+
data-download-src={downloadSrc}
|
| 80 |
+
/>
|
| 81 |
+
</a>
|
| 82 |
+
) : (
|
| 83 |
+
<Image
|
| 84 |
+
{...imgProps}
|
| 85 |
+
data-zoomable={dataZoomable}
|
| 86 |
+
data-downloadable={dataDownloadable}
|
| 87 |
+
data-download-name={downloadName}
|
| 88 |
+
data-download-src={downloadSrc}
|
| 89 |
+
/>
|
| 90 |
+
)}
|
| 91 |
+
<button
|
| 92 |
+
type="button"
|
| 93 |
+
class="button img-dl-btn"
|
| 94 |
+
aria-label="Download image"
|
| 95 |
+
title={
|
| 96 |
+
downloadName ? `Download ${downloadName}` : "Download image"
|
| 97 |
+
}
|
| 98 |
+
>
|
| 99 |
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
| 100 |
+
<path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z" />
|
| 101 |
+
</svg>
|
| 102 |
+
</button>
|
| 103 |
+
</span>
|
| 104 |
+
) : hasLink ? (
|
| 105 |
+
<a
|
| 106 |
+
class="ri-link"
|
| 107 |
+
href={linkHref}
|
| 108 |
+
target={resolvedTarget}
|
| 109 |
+
rel={resolvedRel}
|
| 110 |
+
>
|
| 111 |
<Image {...imgProps} data-zoomable={dataZoomable} />
|
| 112 |
</a>
|
| 113 |
) : (
|
| 114 |
<Image {...imgProps} data-zoomable={dataZoomable} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
)}
|
| 116 |
+
<figcaption>
|
| 117 |
+
{hasCaptionSlot ? (
|
| 118 |
+
<slot name="caption" />
|
| 119 |
+
) : (
|
| 120 |
+
caption && <span set:html={caption} />
|
| 121 |
+
)}
|
| 122 |
+
</figcaption>
|
| 123 |
+
</figure>
|
| 124 |
+
) : dataDownloadable ? (
|
| 125 |
<span class="img-dl-wrap">
|
| 126 |
{hasLink ? (
|
| 127 |
+
<a
|
| 128 |
+
class="ri-link"
|
| 129 |
+
href={linkHref}
|
| 130 |
+
target={resolvedTarget}
|
| 131 |
+
rel={resolvedRel}
|
| 132 |
+
>
|
| 133 |
+
<Image
|
| 134 |
+
{...imgProps}
|
| 135 |
+
data-zoomable={dataZoomable}
|
| 136 |
+
data-downloadable={dataDownloadable}
|
| 137 |
+
data-download-name={downloadName}
|
| 138 |
+
data-download-src={downloadSrc}
|
| 139 |
+
/>
|
| 140 |
</a>
|
| 141 |
) : (
|
| 142 |
+
<Image
|
| 143 |
+
{...imgProps}
|
| 144 |
+
data-zoomable={dataZoomable}
|
| 145 |
+
data-downloadable={dataDownloadable}
|
| 146 |
+
data-download-name={downloadName}
|
| 147 |
+
data-download-src={downloadSrc}
|
| 148 |
+
/>
|
| 149 |
)}
|
| 150 |
+
<button
|
| 151 |
+
type="button"
|
| 152 |
+
class="button img-dl-btn"
|
| 153 |
+
aria-label="Download image"
|
| 154 |
+
title={downloadName ? `Download ${downloadName}` : "Download image"}
|
| 155 |
+
>
|
| 156 |
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
| 157 |
+
<path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z" />
|
| 158 |
+
</svg>
|
| 159 |
</button>
|
| 160 |
</span>
|
| 161 |
+
) : hasLink ? (
|
| 162 |
+
<a
|
| 163 |
+
class="ri-link"
|
| 164 |
+
href={linkHref}
|
| 165 |
+
target={resolvedTarget}
|
| 166 |
+
rel={resolvedRel}
|
| 167 |
+
>
|
| 168 |
<Image {...imgProps} data-zoomable={dataZoomable} />
|
| 169 |
+
</a>
|
| 170 |
+
) : (
|
| 171 |
+
<Image {...imgProps} data-zoomable={dataZoomable} />
|
| 172 |
)
|
| 173 |
+
}
|
| 174 |
</div>
|
| 175 |
|
| 176 |
<script is:inline>
|
| 177 |
(() => {
|
| 178 |
+
console.log("ResponsiveImage script: Starting execution");
|
| 179 |
const scriptEl = document.currentScript;
|
| 180 |
+
console.log("ResponsiveImage script: scriptEl =", scriptEl);
|
| 181 |
const root = scriptEl ? scriptEl.previousElementSibling : null;
|
| 182 |
+
console.log("ResponsiveImage script: root =", root);
|
| 183 |
+
if (!root) {
|
| 184 |
+
console.log("ResponsiveImage script: No root element found, exiting");
|
| 185 |
+
return;
|
| 186 |
+
}
|
| 187 |
+
const img =
|
| 188 |
+
root.tagName === "IMG"
|
| 189 |
+
? root
|
| 190 |
+
: root.querySelector
|
| 191 |
+
? root.querySelector("img")
|
| 192 |
+
: null;
|
| 193 |
+
console.log("ResponsiveImage script: img =", img);
|
| 194 |
+
if (!img) {
|
| 195 |
+
console.log("ResponsiveImage script: No img element found, exiting");
|
| 196 |
+
return;
|
| 197 |
+
}
|
| 198 |
|
| 199 |
// medium-zoom integration scoped to this image only
|
| 200 |
const ensureMediumZoomReady = (cb) => {
|
|
|
|
| 202 |
if (window.mediumZoom) return cb();
|
| 203 |
const retry = () => {
|
| 204 |
// @ts-ignore
|
| 205 |
+
if (window.mediumZoom) cb();
|
| 206 |
+
else setTimeout(retry, 30);
|
| 207 |
};
|
| 208 |
retry();
|
| 209 |
};
|
| 210 |
|
| 211 |
const initZoomIfNeeded = () => {
|
| 212 |
+
console.log("ResponsiveImage: checking zoom for", img);
|
| 213 |
+
console.log(
|
| 214 |
+
"ResponsiveImage: data-zoomable =",
|
| 215 |
+
img.getAttribute("data-zoomable"),
|
| 216 |
+
);
|
| 217 |
+
if (img.getAttribute("data-zoomable") !== "1") return;
|
| 218 |
+
console.log("ResponsiveImage: initializing zoom for", img);
|
| 219 |
+
const isDark =
|
| 220 |
+
document.documentElement.getAttribute("data-theme") === "dark";
|
| 221 |
+
const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";
|
| 222 |
ensureMediumZoomReady(() => {
|
| 223 |
// @ts-ignore
|
| 224 |
+
const instance = window.mediumZoom
|
| 225 |
+
? window.mediumZoom(img, { background, margin: 24, scrollOffset: 0 })
|
| 226 |
+
: null;
|
| 227 |
+
console.log("ResponsiveImage: zoom instance created:", instance);
|
| 228 |
if (!instance) return;
|
| 229 |
let onScrollLike;
|
| 230 |
const attachCloseOnScroll = () => {
|
| 231 |
if (onScrollLike) return;
|
| 232 |
+
onScrollLike = () => {
|
| 233 |
+
try {
|
| 234 |
+
instance.close && instance.close();
|
| 235 |
+
} catch {}
|
| 236 |
+
};
|
| 237 |
+
window.addEventListener("wheel", onScrollLike, { passive: true });
|
| 238 |
+
window.addEventListener("touchmove", onScrollLike, { passive: true });
|
| 239 |
+
window.addEventListener("scroll", onScrollLike, { passive: true });
|
| 240 |
};
|
| 241 |
const detachCloseOnScroll = () => {
|
| 242 |
if (!onScrollLike) return;
|
| 243 |
+
window.removeEventListener("wheel", onScrollLike);
|
| 244 |
+
window.removeEventListener("touchmove", onScrollLike);
|
| 245 |
+
window.removeEventListener("scroll", onScrollLike);
|
| 246 |
onScrollLike = null;
|
| 247 |
};
|
| 248 |
+
try {
|
| 249 |
+
instance.on && instance.on("open", attachCloseOnScroll);
|
| 250 |
+
} catch {}
|
| 251 |
+
try {
|
| 252 |
+
instance.on && instance.on("close", detachCloseOnScroll);
|
| 253 |
+
} catch {}
|
| 254 |
const themeObserver = new MutationObserver(() => {
|
| 255 |
+
const dark =
|
| 256 |
+
document.documentElement.getAttribute("data-theme") === "dark";
|
| 257 |
+
try {
|
| 258 |
+
instance.update &&
|
| 259 |
+
instance.update({
|
| 260 |
+
background: dark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)",
|
| 261 |
+
});
|
| 262 |
+
} catch {}
|
| 263 |
});
|
| 264 |
+
themeObserver.observe(document.documentElement, {
|
| 265 |
+
attributes: true,
|
| 266 |
+
attributeFilter: ["data-theme"],
|
| 267 |
+
});
|
| 268 |
+
});
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
// Gestion zoom global pour masquer autres ResponsiveImage
|
| 272 |
+
const setupGlobalZoomBehavior = () => {
|
| 273 |
+
img.addEventListener("click", () => {
|
| 274 |
+
if (img.getAttribute("data-zoomable") === "1") {
|
| 275 |
+
// Enlever zoom-active de tous les autres ri-root
|
| 276 |
+
document
|
| 277 |
+
.querySelectorAll(".ri-root.zoom-active")
|
| 278 |
+
.forEach((el) => el.classList.remove("zoom-active"));
|
| 279 |
+
|
| 280 |
+
// Ajouter zoom-active Γ cet ri-root
|
| 281 |
+
root.classList.add("zoom-active");
|
| 282 |
+
}
|
| 283 |
});
|
| 284 |
};
|
| 285 |
|
| 286 |
// Download button handler
|
| 287 |
+
const dlBtn = root.querySelector ? root.querySelector(".img-dl-btn") : null;
|
| 288 |
if (dlBtn) {
|
| 289 |
+
dlBtn.addEventListener("click", async (ev) => {
|
| 290 |
try {
|
| 291 |
ev.preventDefault();
|
| 292 |
ev.stopPropagation();
|
| 293 |
const pickHrefAndName = () => {
|
| 294 |
+
const current = img.currentSrc || img.src || "";
|
| 295 |
+
let href = img.getAttribute("data-download-src") || current;
|
| 296 |
const deriveName = () => {
|
| 297 |
try {
|
| 298 |
const u = new URL(current, location.href);
|
| 299 |
+
const rawHref = u.searchParams.get("href");
|
| 300 |
+
const candidate = rawHref
|
| 301 |
+
? decodeURIComponent(rawHref)
|
| 302 |
+
: u.pathname;
|
| 303 |
+
const last = String(candidate).split("/").pop() || "";
|
| 304 |
+
const base = last.split("?")[0].split("#")[0];
|
| 305 |
+
const m = base.match(
|
| 306 |
+
/^(.+?\.(?:png|jpe?g|webp|avif|gif|svg))(?:[._-].*)?$/i,
|
| 307 |
+
);
|
| 308 |
if (m && m[1]) return m[1];
|
| 309 |
+
return base || "image";
|
| 310 |
+
} catch {
|
| 311 |
+
return "image";
|
| 312 |
+
}
|
| 313 |
};
|
| 314 |
+
const name = img.getAttribute("data-download-name") || deriveName();
|
| 315 |
return { href, name };
|
| 316 |
};
|
| 317 |
const picked = pickHrefAndName();
|
| 318 |
+
const res = await fetch(picked.href, { credentials: "same-origin" });
|
| 319 |
const blob = await res.blob();
|
| 320 |
const objectUrl = URL.createObjectURL(blob);
|
| 321 |
+
const tmp = document.createElement("a");
|
| 322 |
tmp.href = objectUrl;
|
| 323 |
+
tmp.download = picked.name || "image";
|
| 324 |
+
tmp.target = "_self";
|
| 325 |
+
tmp.rel = "noopener";
|
| 326 |
+
tmp.style.display = "none";
|
| 327 |
document.body.appendChild(tmp);
|
| 328 |
tmp.click();
|
| 329 |
+
setTimeout(() => {
|
| 330 |
+
URL.revokeObjectURL(objectUrl);
|
| 331 |
+
tmp.remove();
|
| 332 |
+
}, 1000);
|
| 333 |
} catch {}
|
| 334 |
});
|
| 335 |
}
|
| 336 |
|
| 337 |
+
// Setup comportement zoom
|
| 338 |
+
setupGlobalZoomBehavior();
|
| 339 |
+
|
| 340 |
+
if (document.readyState === "complete") initZoomIfNeeded();
|
| 341 |
+
else window.addEventListener("load", initZoomIfNeeded, { once: true });
|
| 342 |
})();
|
| 343 |
</script>
|
| 344 |
|
|
|
|
| 345 |
<style>
|
| 346 |
+
figure {
|
| 347 |
+
margin: var(--block-spacing-y) 0;
|
| 348 |
+
}
|
| 349 |
+
figcaption {
|
| 350 |
+
text-align: left;
|
| 351 |
+
font-size: 0.9rem;
|
| 352 |
+
color: var(--muted-color);
|
| 353 |
+
margin-top: 6px;
|
| 354 |
+
}
|
| 355 |
+
figcaption {
|
| 356 |
+
background: var(--page-bg);
|
| 357 |
+
position: relative;
|
| 358 |
+
z-index: var(--z-elevated);
|
| 359 |
+
display: block;
|
| 360 |
+
width: 100%;
|
| 361 |
+
}
|
| 362 |
+
.image-credit {
|
| 363 |
+
display: block;
|
| 364 |
+
margin-top: 4px;
|
| 365 |
+
font-size: 12px;
|
| 366 |
+
color: var(--muted-color);
|
| 367 |
+
}
|
| 368 |
+
.image-credit a {
|
| 369 |
+
color: inherit;
|
| 370 |
+
text-decoration: underline;
|
| 371 |
+
text-underline-offset: 2px;
|
| 372 |
+
}
|
| 373 |
|
| 374 |
/* Zoomable overlay container (if used by any lightbox implementation) */
|
| 375 |
[data-zoom-overlay],
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
/* Download link inside figures */
|
| 383 |
+
figure .download-link {
|
| 384 |
+
position: relative;
|
| 385 |
+
z-index: var(--z-elevated);
|
| 386 |
+
}
|
| 387 |
|
| 388 |
+
/* Opt-in zoomable images */
|
| 389 |
+
img[data-zoomable] {
|
| 390 |
+
cursor: zoom-in;
|
| 391 |
+
}
|
| 392 |
+
.medium-zoom--opened img[data-zoomable] {
|
| 393 |
+
cursor: zoom-out;
|
| 394 |
+
}
|
| 395 |
|
| 396 |
/* Download button for img[data-downloadable] */
|
| 397 |
+
figure.has-dl-btn {
|
| 398 |
+
position: relative;
|
| 399 |
+
}
|
| 400 |
+
.dl-host {
|
| 401 |
+
position: relative;
|
| 402 |
+
}
|
| 403 |
+
.img-dl-wrap {
|
| 404 |
+
position: relative;
|
| 405 |
+
display: inline-block;
|
| 406 |
+
}
|
| 407 |
.img-dl-btn {
|
| 408 |
position: absolute;
|
| 409 |
right: 8px;
|
|
|
|
| 415 |
border-radius: 6px;
|
| 416 |
color: white;
|
| 417 |
text-decoration: none;
|
| 418 |
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
| 419 |
z-index: var(--z-elevated);
|
| 420 |
display: none;
|
| 421 |
+
background: var(--primary-color);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
/* Quand une image est zoomΓ©e, cacher TOUS les ResponsiveImage de la page */
|
| 425 |
+
:global(.medium-zoom--opened) .ri-root {
|
| 426 |
+
opacity: 0;
|
| 427 |
+
z-index: calc(var(--z-base) - 1);
|
| 428 |
+
transition: opacity 0.3s ease;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* L'image actuellement zoomΓ©e reste visible */
|
| 432 |
+
:global(.medium-zoom--opened) .ri-root:has(.medium-zoom--opened) {
|
| 433 |
+
opacity: 1;
|
| 434 |
+
z-index: var(--z-overlay);
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
/* Fallback pour navigateurs sans support :has() */
|
| 438 |
+
:global(.medium-zoom--opened) .ri-root.zoom-active {
|
| 439 |
+
opacity: 1 !important;
|
| 440 |
+
z-index: var(--z-overlay) !important;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* SpΓ©cifiquement masquer bouton download et figcaption lors du zoom */
|
| 444 |
+
:global(.medium-zoom--opened) .img-dl-btn {
|
| 445 |
+
opacity: 0;
|
| 446 |
+
z-index: calc(var(--z-base) - 1);
|
| 447 |
+
transition: opacity 0.3s ease;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
:global(.medium-zoom--opened) figcaption {
|
| 451 |
+
opacity: 0;
|
| 452 |
+
z-index: calc(var(--z-base) - 1);
|
| 453 |
+
transition: opacity 0.3s ease;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
/* MΓͺme pour l'image zoomΓ©e active, masquer bouton et caption pour une expΓ©rience propre */
|
| 457 |
+
:global(.medium-zoom--opened) .ri-root.zoom-active .img-dl-btn {
|
| 458 |
+
opacity: 0;
|
| 459 |
+
z-index: calc(var(--z-base) - 1);
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
:global(.medium-zoom--opened) .ri-root.zoom-active figcaption {
|
| 463 |
+
opacity: 0;
|
| 464 |
+
z-index: calc(var(--z-base) - 1);
|
| 465 |
+
}
|
| 466 |
+
.img-dl-btn svg {
|
| 467 |
+
width: 18px;
|
| 468 |
+
height: 18px;
|
| 469 |
+
fill: currentColor;
|
| 470 |
+
}
|
| 471 |
+
.img-dl-wrap:hover .img-dl-btn {
|
| 472 |
+
display: inline-flex;
|
| 473 |
+
}
|
| 474 |
+
.img-dl-btn:hover {
|
| 475 |
+
background: var(--primary-color-hover);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
[data-theme="dark"] .img-dl-btn {
|
| 479 |
+
background: var(--primary-color);
|
| 480 |
+
color: var(--on-primary);
|
| 481 |
+
border-color: var(--primary-color);
|
| 482 |
+
}
|
| 483 |
+
[data-theme="dark"] .img-dl-btn:hover {
|
| 484 |
+
background: var(--primary-color-hover);
|
| 485 |
}
|
| 486 |
+
</style>
|
|
|
|
|
|
|
|
|
|
|
|
app/src/components/Sidenote.astro
CHANGED
|
@@ -1,37 +1,78 @@
|
|
| 1 |
---
|
|
|
|
| 2 |
---
|
| 3 |
-
|
| 4 |
-
|
|
|
|
| 5 |
<slot />
|
| 6 |
-
</div>
|
| 7 |
-
<aside class="aside__aside">
|
| 8 |
-
<slot name="aside" />
|
| 9 |
</aside>
|
| 10 |
</div>
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
<style is:global>
|
| 14 |
-
.
|
| 15 |
-
|
|
|
|
| 16 |
margin: 12px 0;
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
-
.
|
| 20 |
-
position: absolute;
|
| 21 |
-
top: 0;
|
| 22 |
-
right: -260px; /* push into the right grid column (width 260 + gap 32) */
|
| 23 |
-
width: 260px;
|
| 24 |
border-radius: 8px;
|
| 25 |
padding: 0 30px;
|
| 26 |
font-size: 0.9rem;
|
| 27 |
color: var(--muted-color);
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
@media (--bp-content-collapse) {
|
| 31 |
-
.
|
| 32 |
-
position: static;
|
| 33 |
-
width: auto;
|
|
|
|
|
|
|
| 34 |
margin-top: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
}
|
| 37 |
</style>
|
|
|
|
| 1 |
---
|
| 2 |
+
|
| 3 |
---
|
| 4 |
+
|
| 5 |
+
<div class="sidenote-container">
|
| 6 |
+
<aside class="sidenote">
|
| 7 |
<slot />
|
|
|
|
|
|
|
|
|
|
| 8 |
</aside>
|
| 9 |
</div>
|
| 10 |
|
| 11 |
+
<script>
|
| 12 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 13 |
+
const containers = document.querySelectorAll(".sidenote-container");
|
| 14 |
+
|
| 15 |
+
containers.forEach((container) => {
|
| 16 |
+
// Trouve l'élément précédent (frère juste avant)
|
| 17 |
+
const previousElement = container.previousElementSibling;
|
| 18 |
+
|
| 19 |
+
if (previousElement) {
|
| 20 |
+
// Rend le conteneur de la sidenote relatif Γ l'Γ©lΓ©ment prΓ©cΓ©dent
|
| 21 |
+
previousElement.style.position = "relative";
|
| 22 |
+
|
| 23 |
+
// DΓ©place le conteneur de la sidenote comme enfant de l'Γ©lΓ©ment prΓ©cΓ©dent
|
| 24 |
+
previousElement.appendChild(container);
|
| 25 |
+
|
| 26 |
+
// Style le conteneur pour qu'il se positionne correctement
|
| 27 |
+
container.style.position = "absolute";
|
| 28 |
+
container.style.top = "0";
|
| 29 |
+
container.style.right = "-292px"; // 260px width + 32px gap
|
| 30 |
+
container.style.width = "260px";
|
| 31 |
+
|
| 32 |
+
// Affiche le container avec un fade-in
|
| 33 |
+
container.style.display = "block";
|
| 34 |
+
container.style.opacity = "0";
|
| 35 |
+
|
| 36 |
+
// Fade-in avec transition
|
| 37 |
+
setTimeout(() => {
|
| 38 |
+
container.style.opacity = "1";
|
| 39 |
+
}, 10);
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
});
|
| 43 |
+
</script>
|
| 44 |
|
| 45 |
<style is:global>
|
| 46 |
+
.sidenote-container {
|
| 47 |
+
/* CachΓ© par dΓ©faut, sera affichΓ© par JS */
|
| 48 |
+
display: none;
|
| 49 |
margin: 12px 0;
|
| 50 |
+
/* Transition pour le fade-in */
|
| 51 |
+
transition: opacity 0.3s ease-in-out;
|
| 52 |
}
|
| 53 |
|
| 54 |
+
.sidenote {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
border-radius: 8px;
|
| 56 |
padding: 0 30px;
|
| 57 |
font-size: 0.9rem;
|
| 58 |
color: var(--muted-color);
|
| 59 |
+
margin: 0;
|
| 60 |
}
|
| 61 |
|
| 62 |
@media (--bp-content-collapse) {
|
| 63 |
+
.sidenote-container {
|
| 64 |
+
position: static !important;
|
| 65 |
+
width: auto !important;
|
| 66 |
+
right: auto !important;
|
| 67 |
+
top: auto !important;
|
| 68 |
margin-top: 8px;
|
| 69 |
+
/* Affichage normal sur mobile */
|
| 70 |
+
display: block !important;
|
| 71 |
+
opacity: 1 !important;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.sidenote {
|
| 75 |
+
padding: 0;
|
| 76 |
}
|
| 77 |
}
|
| 78 |
</style>
|
app/src/content/article.mdx
CHANGED
|
@@ -20,15 +20,15 @@ tags:
|
|
| 20 |
tableOfContentsAutoCollapse: true
|
| 21 |
---
|
| 22 |
|
| 23 |
-
import Introduction from "./chapters/introduction.mdx";
|
| 24 |
-
import BestPractices from "./chapters/best-pratices.mdx";
|
| 25 |
-
import WritingYourContent from "./chapters/writing-your-content.mdx";
|
| 26 |
-
import AvailableBlocks from "./chapters/markdown.mdx";
|
| 27 |
-
import GettingStarted from "./chapters/getting-started.mdx";
|
| 28 |
-
import Markdown from "./chapters/markdown.mdx";
|
| 29 |
-
import Components from "./chapters/components.mdx";
|
| 30 |
-
import Greetings from "./chapters/greetings.mdx";
|
| 31 |
-
import VibeCodingCharts from "./chapters/vibe-coding-charts.mdx";
|
| 32 |
|
| 33 |
<Introduction />
|
| 34 |
|
|
|
|
| 20 |
tableOfContentsAutoCollapse: true
|
| 21 |
---
|
| 22 |
|
| 23 |
+
import Introduction from "./chapters/demo/introduction.mdx";
|
| 24 |
+
import BestPractices from "./chapters/demo/best-pratices.mdx";
|
| 25 |
+
import WritingYourContent from "./chapters/demo/writing-your-content.mdx";
|
| 26 |
+
import AvailableBlocks from "./chapters/demo/markdown.mdx";
|
| 27 |
+
import GettingStarted from "./chapters/demo/getting-started.mdx";
|
| 28 |
+
import Markdown from "./chapters/demo/markdown.mdx";
|
| 29 |
+
import Components from "./chapters/demo/components.mdx";
|
| 30 |
+
import Greetings from "./chapters/demo/greetings.mdx";
|
| 31 |
+
import VibeCodingCharts from "./chapters/demo/vibe-coding-charts.mdx";
|
| 32 |
|
| 33 |
<Introduction />
|
| 34 |
|
app/src/content/assets/data/llm_benchmarks.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:67f10bd2fdcce7231c40db14b013a587e0dcecf02e427b10bb159669576e9ad8
|
| 3 |
+
size 834
|
app/src/content/chapters/{best-pratices.mdx β demo/best-pratices.mdx}
RENAMED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
|
| 2 |
-
import visualPoster from '
|
| 3 |
-
import Note from '
|
| 4 |
-
import ResponsiveImage from '
|
|
|
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
## Best Practices
|
|
@@ -36,26 +38,22 @@ Favor **concise captions** and callouts that clarify what to look at and why it
|
|
| 36 |
|
| 37 |
</div>
|
| 38 |
|
| 39 |
-
|
|
|
|
| 40 |
A palette encodes **meaning** (categories, magnitudes, oppositions), preserves **readability** and **accessibility** (sufficient contrast, colorβvision safety), and ensures **perceptually smooth transitions**. The three families below illustrate when to use **categorical**, **sequential**, or **diverging** colors and how they evolve from the same **reference hue**.
|
| 41 |
|
| 42 |
-
<Sidenote>
|
| 43 |
<div className="">
|
| 44 |
<HtmlEmbed src="palettes.html" />
|
| 45 |
</div>
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
</
|
| 49 |
-
|
| 50 |
-
It will be applied to the <a href="/" target="_blank">whole page</a>.
|
| 51 |
-
</Fragment>
|
| 52 |
-
</Sidenote> */}
|
| 53 |
|
| 54 |
### Use the right chart
|
| 55 |
|
| 56 |
Picking the right visualization depends on your goal (compare values, show distribution, part-to-whole, trends, relationships, etc.). The Visual Vocabulary poster below provides a concise mapping from **analytical task** to **chart types**.
|
| 57 |
|
| 58 |
-
|
| 59 |
<ResponsiveImage
|
| 60 |
src={visualPoster}
|
| 61 |
alt="Visual Vocabulary: choosing the right chart by task"
|
|
|
|
| 1 |
|
| 2 |
+
import visualPoster from '../../assets/image/visual-vocabulary-poster.png';
|
| 3 |
+
import Note from '../../../components/Note.astro';
|
| 4 |
+
import ResponsiveImage from '../../../components/ResponsiveImage.astro';
|
| 5 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro';
|
| 6 |
+
import Sidenote from '../../../components/Sidenote.astro';
|
| 7 |
|
| 8 |
|
| 9 |
## Best Practices
|
|
|
|
| 38 |
|
| 39 |
</div>
|
| 40 |
|
| 41 |
+
### Use the right color
|
| 42 |
+
|
| 43 |
A palette encodes **meaning** (categories, magnitudes, oppositions), preserves **readability** and **accessibility** (sufficient contrast, colorβvision safety), and ensures **perceptually smooth transitions**. The three families below illustrate when to use **categorical**, **sequential**, or **diverging** colors and how they evolve from the same **reference hue**.
|
| 44 |
|
|
|
|
| 45 |
<div className="">
|
| 46 |
<HtmlEmbed src="palettes.html" />
|
| 47 |
</div>
|
| 48 |
+
|
| 49 |
+
<Sidenote>
|
| 50 |
+
You can choose a color from the palette to update palettes and copy them to your clipboard. It will be applied to the <a href="/" target="_blank">whole page</a>.
|
| 51 |
+
</Sidenote>
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
### Use the right chart
|
| 54 |
|
| 55 |
Picking the right visualization depends on your goal (compare values, show distribution, part-to-whole, trends, relationships, etc.). The Visual Vocabulary poster below provides a concise mapping from **analytical task** to **chart types**.
|
| 56 |
|
|
|
|
| 57 |
<ResponsiveImage
|
| 58 |
src={visualPoster}
|
| 59 |
alt="Visual Vocabulary: choosing the right chart by task"
|
app/src/content/chapters/{components.mdx β demo/components.mdx}
RENAMED
|
@@ -1,35 +1,79 @@
|
|
| 1 |
import { Image } from 'astro:assets';
|
| 2 |
-
import placeholder from '
|
| 3 |
-
import audioDemo from '
|
| 4 |
-
import HtmlEmbed from '
|
| 5 |
-
import Sidenote from '
|
| 6 |
-
import Wide from '
|
| 7 |
-
import Note from '
|
| 8 |
-
import FullWidth from '
|
| 9 |
-
import Accordion from '
|
| 10 |
-
import
|
|
|
|
|
|
|
| 11 |
|
| 12 |
## Components
|
| 13 |
|
| 14 |
**All** the following **components** are available in the **article.mdx** file. You can also create your own **components** by creating a new file in the `/components` folder.
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</div>
|
| 26 |
|
| 27 |
-
###
|
| 28 |
|
| 29 |
-
**
|
| 30 |
|
| 31 |
|
| 32 |
-
<
|
| 33 |
src={placeholder}
|
| 34 |
zoomable
|
| 35 |
downloadable
|
|
@@ -50,12 +94,12 @@ You have to import them in the **.mdx** file you want to use them in.
|
|
| 50 |
|
| 51 |
<Accordion title="Code example">
|
| 52 |
```mdx
|
| 53 |
-
import
|
| 54 |
import myImage from './assets/image/placeholder.jpg'
|
| 55 |
|
| 56 |
-
<
|
| 57 |
|
| 58 |
-
<
|
| 59 |
src={myImage}
|
| 60 |
layout="fixed"
|
| 61 |
zoomable
|
|
@@ -67,6 +111,40 @@ import myImage from './assets/image/placeholder.jpg'
|
|
| 67 |
```
|
| 68 |
</Accordion>
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
### Placement
|
| 72 |
|
|
@@ -74,21 +152,18 @@ Use these helpers when you need to step outside the main content flow: **Sidenot
|
|
| 74 |
|
| 75 |
#### Sidenotes
|
| 76 |
|
|
|
|
|
|
|
| 77 |
<Sidenote>
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
**Side note** for brief context or a definition.
|
| 81 |
-
</Fragment>
|
| 82 |
-
</Sidenote>
|
| 83 |
|
| 84 |
<Accordion title="Code example">
|
| 85 |
```mdx
|
| 86 |
-
import Sidenote from '
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
<Fragment slot="aside">Short side note.</Fragment>
|
| 91 |
-
</Sidenote>
|
| 92 |
```
|
| 93 |
</Accordion>
|
| 94 |
|
|
@@ -101,7 +176,7 @@ import Sidenote from '../components/Sidenote.astro'
|
|
| 101 |
|
| 102 |
<Accordion title="Code example">
|
| 103 |
```mdx
|
| 104 |
-
import Wide from '
|
| 105 |
|
| 106 |
<Wide>
|
| 107 |
Your content here...
|
|
@@ -117,7 +192,7 @@ import Wide from '../components/Wide.astro'
|
|
| 117 |
|
| 118 |
<Accordion title="Code example">
|
| 119 |
```mdx
|
| 120 |
-
import FullWidth from '
|
| 121 |
|
| 122 |
<FullWidth>
|
| 123 |
Your content here...
|
|
@@ -151,7 +226,7 @@ Can be used like this `<Accordion>some content</Accordion>`. You can pass any ch
|
|
| 151 |
|
| 152 |
<Accordion title="Code example">
|
| 153 |
````mdx
|
| 154 |
-
import Accordion from '
|
| 155 |
|
| 156 |
<Accordion title="Accordion title" open>
|
| 157 |
<p>Free content with <strong>markdown</strong> and MDX components.</p>
|
|
@@ -209,7 +284,7 @@ Small contextual callout for tips, caveats, or emphasis.
|
|
| 209 |
|
| 210 |
<Accordion title="Code example">
|
| 211 |
```mdx
|
| 212 |
-
import Note from '
|
| 213 |
|
| 214 |
<Note title="Headsβup" emoji="π‘" variant="info">
|
| 215 |
Use notes to surface context without breaking reading flow.
|
|
@@ -229,6 +304,28 @@ import Note from '../../components/Note.astro'
|
|
| 229 |
```
|
| 230 |
</Accordion>
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
### Iframes
|
| 234 |
|
|
@@ -274,7 +371,7 @@ For researchers who want to stay in **Python** while targeting **D3**, the [d3bl
|
|
| 274 |
|
| 275 |
<Accordion title="Code example">
|
| 276 |
```mdx
|
| 277 |
-
import HtmlEmbed from '
|
| 278 |
|
| 279 |
<HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Some chart description <br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
|
| 280 |
|
|
|
|
| 1 |
import { Image } from 'astro:assets';
|
| 2 |
+
import placeholder from '../../assets/image/placeholder.png';
|
| 3 |
+
import audioDemo from '../../assets/audio/audio-example.wav';
|
| 4 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro';
|
| 5 |
+
import Sidenote from '../../../components/Sidenote.astro';
|
| 6 |
+
import Wide from '../../../components/Wide.astro';
|
| 7 |
+
import Note from '../../../components/Note.astro';
|
| 8 |
+
import FullWidth from '../../../components/FullWidth.astro';
|
| 9 |
+
import Accordion from '../../../components/Accordion.astro';
|
| 10 |
+
import Figure from '../../../components/ResponsiveImage.astro';
|
| 11 |
+
import SubFigures from '../../../components/MultiImage.astro';
|
| 12 |
+
import Quote from '../../../components/Quote.astro';
|
| 13 |
|
| 14 |
## Components
|
| 15 |
|
| 16 |
**All** the following **components** are available in the **article.mdx** file. You can also create your own **components** by creating a new file in the `/components` folder.
|
| 17 |
+
|
| 18 |
+
### How to import components
|
| 19 |
+
|
| 20 |
+
To use any component in your MDX file, add the import statement at the top:
|
| 21 |
+
|
| 22 |
+
```mdx
|
| 23 |
+
import Figure from '../components/ResponsiveImage.astro';
|
| 24 |
+
import SubFigures from '../components/MultiImage.astro';
|
| 25 |
+
import Note from '../components/Note.astro';
|
| 26 |
+
|
| 27 |
+
# Your content
|
| 28 |
+
|
| 29 |
+
<Figure src={myImage} alt="Description" />
|
| 30 |
+
<Note>This is a note</Note>
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
Here are the components that are available:
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
<div className="feature-grid">
|
| 37 |
+
<a href="#figure" className="feature-card">
|
| 38 |
+
<strong>Figure</strong>
|
| 39 |
+
<span>Optimized images</span>
|
| 40 |
+
</a>
|
| 41 |
+
<a href="#subfigures" className="feature-card">
|
| 42 |
+
<strong>SubFigures</strong>
|
| 43 |
+
<span>Multi-panel figures</span>
|
| 44 |
+
</a>
|
| 45 |
+
<a href="#placement" className="feature-card">
|
| 46 |
+
<strong>Placement</strong>
|
| 47 |
+
<span>Layout helpers</span>
|
| 48 |
+
</a>
|
| 49 |
+
<a href="#accordion" className="feature-card">
|
| 50 |
+
<strong>Accordion</strong>
|
| 51 |
+
<span>Collapsible content</span>
|
| 52 |
+
</a>
|
| 53 |
+
<a href="#note" className="feature-card">
|
| 54 |
+
<strong>Note</strong>
|
| 55 |
+
<span>Contextual callouts</span>
|
| 56 |
+
</a>
|
| 57 |
+
<a href="#quote" className="feature-card">
|
| 58 |
+
<strong>Quote</strong>
|
| 59 |
+
<span>Elegant citations</span>
|
| 60 |
+
</a>
|
| 61 |
+
<a href="#htmlembed" className="feature-card">
|
| 62 |
+
<strong>HtmlEmbed</strong>
|
| 63 |
+
<span>External content</span>
|
| 64 |
+
</a>
|
| 65 |
+
<a href="#iframe" className="feature-card">
|
| 66 |
+
<strong>Iframe</strong>
|
| 67 |
+
<span>Web embeds</span>
|
| 68 |
+
</a>
|
| 69 |
</div>
|
| 70 |
|
| 71 |
+
### Figure
|
| 72 |
|
| 73 |
+
**Figures** automatically generate optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
|
| 74 |
|
| 75 |
|
| 76 |
+
<Figure
|
| 77 |
src={placeholder}
|
| 78 |
zoomable
|
| 79 |
downloadable
|
|
|
|
| 94 |
|
| 95 |
<Accordion title="Code example">
|
| 96 |
```mdx
|
| 97 |
+
import Figure from '../../../components/ResponsiveImage.astro'
|
| 98 |
import myImage from './assets/image/placeholder.jpg'
|
| 99 |
|
| 100 |
+
<Figure src={myImage} alt="Optimized figure with caption" />
|
| 101 |
|
| 102 |
+
<Figure
|
| 103 |
src={myImage}
|
| 104 |
layout="fixed"
|
| 105 |
zoomable
|
|
|
|
| 111 |
```
|
| 112 |
</Accordion>
|
| 113 |
|
| 114 |
+
### SubFigures
|
| 115 |
+
|
| 116 |
+
**Display multiple figures** in a grid layout with customizable columns and captions. Perfect for **multi-panel figures**, **comparisons**, and **step-by-step visualizations** β just like LaTeX's `subfigure` environment.
|
| 117 |
+
|
| 118 |
+
<SubFigures
|
| 119 |
+
images={[
|
| 120 |
+
{ src: placeholder, alt: "Image 1", caption: "First image with caption" },
|
| 121 |
+
{ src: placeholder, alt: "Image 2", caption: "Second image" },
|
| 122 |
+
{ src: placeholder, alt: "Image 3", caption: "Third image description" }
|
| 123 |
+
]}
|
| 124 |
+
layout="3-column"
|
| 125 |
+
caption="Gallery of three images with individual captions"
|
| 126 |
+
zoomable={true}
|
| 127 |
+
/>
|
| 128 |
+
|
| 129 |
+
<Accordion title="Code example">
|
| 130 |
+
```mdx
|
| 131 |
+
import placeholder from '../assets/image/placeholder.png';
|
| 132 |
+
|
| 133 |
+
<SubFigures
|
| 134 |
+
images={[
|
| 135 |
+
{ src: placeholder, alt: "Panel A", caption: "(a) First panel" },
|
| 136 |
+
{ src: placeholder, alt: "Panel B", caption: "(b) Second panel" },
|
| 137 |
+
{ src: placeholder, alt: "Panel C", caption: "(c) Third panel" }
|
| 138 |
+
]}
|
| 139 |
+
layout="3-column"
|
| 140 |
+
caption="Figure 1: Multi-panel comparison showing different conditions"
|
| 141 |
+
zoomable={true}
|
| 142 |
+
/>
|
| 143 |
+
```
|
| 144 |
+
</Accordion>
|
| 145 |
+
|
| 146 |
+
**Layout options:** `2-column`, `3-column`, `4-column`, `auto`
|
| 147 |
+
**Features:** Individual subcaptions (a), (b), (c), zoom functionality, responsive design
|
| 148 |
|
| 149 |
### Placement
|
| 150 |
|
|
|
|
| 152 |
|
| 153 |
#### Sidenotes
|
| 154 |
|
| 155 |
+
This paragraph presents a **key idea** concisely.
|
| 156 |
+
|
| 157 |
<Sidenote>
|
| 158 |
+
**Side note** for brief context or a definition.
|
| 159 |
+
</Sidenote>
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
<Accordion title="Code example">
|
| 162 |
```mdx
|
| 163 |
+
import Sidenote from '../../../components/Sidenote.astro'
|
| 164 |
|
| 165 |
+
Main paragraph with the core idea.
|
| 166 |
+
<Sidenote>Short side note.</Sidenote>
|
|
|
|
|
|
|
| 167 |
```
|
| 168 |
</Accordion>
|
| 169 |
|
|
|
|
| 176 |
|
| 177 |
<Accordion title="Code example">
|
| 178 |
```mdx
|
| 179 |
+
import Wide from '../../../components/Wide.astro'
|
| 180 |
|
| 181 |
<Wide>
|
| 182 |
Your content here...
|
|
|
|
| 192 |
|
| 193 |
<Accordion title="Code example">
|
| 194 |
```mdx
|
| 195 |
+
import FullWidth from '../../../components/FullWidth.astro'
|
| 196 |
|
| 197 |
<FullWidth>
|
| 198 |
Your content here...
|
|
|
|
| 226 |
|
| 227 |
<Accordion title="Code example">
|
| 228 |
````mdx
|
| 229 |
+
import Accordion from '../../../components/Accordion.astro'
|
| 230 |
|
| 231 |
<Accordion title="Accordion title" open>
|
| 232 |
<p>Free content with <strong>markdown</strong> and MDX components.</p>
|
|
|
|
| 284 |
|
| 285 |
<Accordion title="Code example">
|
| 286 |
```mdx
|
| 287 |
+
import Note from '../../../components/Note.astro'
|
| 288 |
|
| 289 |
<Note title="Headsβup" emoji="π‘" variant="info">
|
| 290 |
Use notes to surface context without breaking reading flow.
|
|
|
|
| 304 |
```
|
| 305 |
</Accordion>
|
| 306 |
|
| 307 |
+
### Quote
|
| 308 |
+
|
| 309 |
+
Elegant quotes with optional author attribution.
|
| 310 |
+
|
| 311 |
+
<Quote author="Geoffrey Hinton">
|
| 312 |
+
The brain has about 10^14 synapses and we only live for about 10^9 seconds. So we have a lot more parameters than data. This motivates the idea that we must do a lot of unsupervised learning since labeled examples are so limited.
|
| 313 |
+
</Quote>
|
| 314 |
+
|
| 315 |
+
| Prop | Required | Type | Description |
|
| 316 |
+
|----------|----------|--------|--------------------------------|
|
| 317 |
+
| `author` | No | string | Author of the quote |
|
| 318 |
+
| `source` | No | string | Source publication or context |
|
| 319 |
+
|
| 320 |
+
<Accordion title="Code example">
|
| 321 |
+
```mdx
|
| 322 |
+
import Quote from '../../../components/Quote.astro'
|
| 323 |
+
|
| 324 |
+
<Quote author="Geoffrey Hinton">
|
| 325 |
+
The brain has about 10^14 synapses and we only live for about 10^9 seconds. So we have a lot more parameters than data. This motivates the idea that we must do a lot of unsupervised learning since labeled examples are so limited.
|
| 326 |
+
</Quote>
|
| 327 |
+
```
|
| 328 |
+
</Accordion>
|
| 329 |
|
| 330 |
### Iframes
|
| 331 |
|
|
|
|
| 371 |
|
| 372 |
<Accordion title="Code example">
|
| 373 |
```mdx
|
| 374 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro'
|
| 375 |
|
| 376 |
<HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Some chart description <br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
|
| 377 |
|
app/src/content/chapters/{debug-components.mdx β demo/debug-components.mdx}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
import Accordion from '
|
| 2 |
-
import HtmlEmbed from '
|
| 3 |
-
import ResponsiveImage from '
|
| 4 |
-
import Wide from '
|
| 5 |
-
import FullWidth from '
|
| 6 |
-
import Note from '
|
| 7 |
|
| 8 |
| Prop | Required |
|
| 9 |
|------------------------|----------|
|
|
@@ -29,7 +29,7 @@ import Note from '../../components/Note.astro';
|
|
| 29 |
|
| 30 |
<Accordion title="code">
|
| 31 |
```mdx
|
| 32 |
-
import HtmlEmbed from '
|
| 33 |
|
| 34 |
<HtmlEmbed src="plotly-line.html" title="Plotly Line" desc="Some chart description" />
|
| 35 |
<HtmlEmbed src="d3-line-example.html" title="D3 Line" desc="Some chart description" />
|
|
|
|
| 1 |
+
import Accordion from '../../../components/Accordion.astro';
|
| 2 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro';
|
| 3 |
+
import ResponsiveImage from '../../../components/ResponsiveImage.astro';
|
| 4 |
+
import Wide from '../../../components/Wide.astro';
|
| 5 |
+
import FullWidth from '../../../components/FullWidth.astro';
|
| 6 |
+
import Note from '../../../components/Note.astro';
|
| 7 |
|
| 8 |
| Prop | Required |
|
| 9 |
|------------------------|----------|
|
|
|
|
| 29 |
|
| 30 |
<Accordion title="code">
|
| 31 |
```mdx
|
| 32 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro'
|
| 33 |
|
| 34 |
<HtmlEmbed src="plotly-line.html" title="Plotly Line" desc="Some chart description" />
|
| 35 |
<HtmlEmbed src="d3-line-example.html" title="D3 Line" desc="Some chart description" />
|
app/src/content/chapters/{getting-started.mdx β demo/getting-started.mdx}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import Sidenote from '
|
| 2 |
-
import Note from '
|
| 3 |
|
| 4 |
## Getting Started
|
| 5 |
|
|
@@ -34,14 +34,14 @@ git lfs pull
|
|
| 34 |
|
| 35 |
|
| 36 |
6. Install dependencies.
|
| 37 |
-
|
| 38 |
```bash
|
| 39 |
cd app
|
| 40 |
npm install
|
| 41 |
```
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
</Sidenote>
|
| 46 |
|
| 47 |
|
|
|
|
| 1 |
+
import Sidenote from '../../../components/Sidenote.astro';
|
| 2 |
+
import Note from '../../../components/Note.astro';
|
| 3 |
|
| 4 |
## Getting Started
|
| 5 |
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
6. Install dependencies.
|
| 37 |
+
|
| 38 |
```bash
|
| 39 |
cd app
|
| 40 |
npm install
|
| 41 |
```
|
| 42 |
+
|
| 43 |
+
<Sidenote>
|
| 44 |
+
Alternatively, you can use **Yarn** as your package manager.
|
| 45 |
</Sidenote>
|
| 46 |
|
| 47 |
|
app/src/content/chapters/{greetings.mdx β demo/greetings.mdx}
RENAMED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
Huge thanks to the following people for their **precious feedbacks**!
|
| 4 |
|
| 5 |
-
import HfUser from '
|
| 6 |
|
| 7 |
<div className="hf-user-list">
|
| 8 |
<HfUser username="lvwerra" name="Leandro von Werra" />
|
|
|
|
| 2 |
|
| 3 |
Huge thanks to the following people for their **precious feedbacks**!
|
| 4 |
|
| 5 |
+
import HfUser from '../../../components/HfUser.astro';
|
| 6 |
|
| 7 |
<div className="hf-user-list">
|
| 8 |
<HfUser username="lvwerra" name="Leandro von Werra" />
|
app/src/content/chapters/{introduction.mdx β demo/introduction.mdx}
RENAMED
|
@@ -1,18 +1,15 @@
|
|
| 1 |
-
import Sidenote from "
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
Grounded in up to date good practices in web dev, it favors **interactive explanations**, **clear notation**, and **inspectable examples** over static snapshots.
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
</Fragment>
|
| 11 |
</Sidenote>
|
| 12 |
|
| 13 |
#### Features
|
| 14 |
|
| 15 |
-
<Sidenote>
|
| 16 |
<div className="tag-list">
|
| 17 |
<span className="tag">Markdown-based</span>
|
| 18 |
<span className="tag">KaTeX math</span>
|
|
@@ -34,9 +31,9 @@ import Sidenote from "../../components/Sidenote.astro";
|
|
| 34 |
<span className="tag">Dark theme</span>
|
| 35 |
<span className="tag">Mobile friendly</span>
|
| 36 |
</div>
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
</
|
| 40 |
</Sidenote>
|
| 41 |
|
| 42 |
## Introduction
|
|
@@ -55,7 +52,6 @@ This is not a CMS or a multiβpage blogβit's a **focused**, **singleβpage**
|
|
| 55 |
|
| 56 |
### Inspired by Distill
|
| 57 |
|
| 58 |
-
<Sidenote>
|
| 59 |
This project stands in the direct continuity of [Distill](https://distill.pub/) (2016β2021). Our goal is to carry that spirit forward and push it even further: **accessible scientific writing**, **highβquality interactive explanations**, and **reproducible**, productionβready demos.
|
| 60 |
|
| 61 |
To give you a sense of what inspired this template, here is a short, curated list of **wellβdesigned** and often **interactive** works from Distill:
|
|
@@ -65,7 +61,6 @@ To give you a sense of what inspired this template, here is a short, curated lis
|
|
| 65 |
- [Handwriting with a Neural Network](https://distill.pub/2016/handwriting/)
|
| 66 |
- [The Building Blocks of Interpretability](https://distill.pub/2018/building-blocks/)
|
| 67 |
|
| 68 |
-
<
|
| 69 |
-
|
| 70 |
-
</Fragment>
|
| 71 |
</Sidenote>
|
|
|
|
| 1 |
+
import Sidenote from "../../../components/Sidenote.astro";
|
| 2 |
|
| 3 |
+
Welcome to this singleβpage **research article template**. It helps you publish **clear**, **modern**, and **interactive technical writing** with **minimal setup**.
|
| 4 |
+
|
| 5 |
+
Grounded in up to date good practices in web dev, it favors **interactive explanations**, **clear notation**, and **inspectable examples** over static snapshots.
|
|
|
|
| 6 |
|
| 7 |
+
<Sidenote>
|
| 8 |
+
Reading time: 20β25 minutes.
|
|
|
|
| 9 |
</Sidenote>
|
| 10 |
|
| 11 |
#### Features
|
| 12 |
|
|
|
|
| 13 |
<div className="tag-list">
|
| 14 |
<span className="tag">Markdown-based</span>
|
| 15 |
<span className="tag">KaTeX math</span>
|
|
|
|
| 31 |
<span className="tag">Dark theme</span>
|
| 32 |
<span className="tag">Mobile friendly</span>
|
| 33 |
</div>
|
| 34 |
+
|
| 35 |
+
<Sidenote>
|
| 36 |
+
If you have questions, remarks or suggestions, open a discussion on the <a href="https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=discussion">Community tab</a>!
|
| 37 |
</Sidenote>
|
| 38 |
|
| 39 |
## Introduction
|
|
|
|
| 52 |
|
| 53 |
### Inspired by Distill
|
| 54 |
|
|
|
|
| 55 |
This project stands in the direct continuity of [Distill](https://distill.pub/) (2016β2021). Our goal is to carry that spirit forward and push it even further: **accessible scientific writing**, **highβquality interactive explanations**, and **reproducible**, productionβready demos.
|
| 56 |
|
| 57 |
To give you a sense of what inspired this template, here is a short, curated list of **wellβdesigned** and often **interactive** works from Distill:
|
|
|
|
| 61 |
- [Handwriting with a Neural Network](https://distill.pub/2016/handwriting/)
|
| 62 |
- [The Building Blocks of Interpretability](https://distill.pub/2018/building-blocks/)
|
| 63 |
|
| 64 |
+
<Sidenote>
|
| 65 |
+
I'm always excited to discover more great examplesβplease share your favorites in the <a href="https://huggingface.co/spaces/tfrere/research-blog-template/discussions?status=open&type=discussion">Community tab</a>!
|
|
|
|
| 66 |
</Sidenote>
|
app/src/content/chapters/{markdown.mdx β demo/markdown.mdx}
RENAMED
|
@@ -1,27 +1,64 @@
|
|
| 1 |
-
import placeholder from '
|
| 2 |
-
import audioDemo from '
|
| 3 |
-
import HtmlEmbed from '
|
| 4 |
-
import Sidenote from '
|
| 5 |
-
import Wide from '
|
| 6 |
-
import Note from '
|
| 7 |
-
import FullWidth from '
|
| 8 |
-
import Accordion from '
|
| 9 |
-
import ResponsiveImage from '
|
| 10 |
|
| 11 |
## Markdown
|
| 12 |
|
| 13 |
-
All the following **markdown features** are available **natively** in the `article.mdx` file.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</div>
|
| 26 |
|
| 27 |
### Math
|
|
@@ -32,6 +69,18 @@ $$
|
|
| 32 |
\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V
|
| 33 |
$$
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
<Accordion title="Code example">
|
| 36 |
```mdx
|
| 37 |
$x^2 + y^2 = z^2$
|
|
@@ -39,6 +88,16 @@ $x^2 + y^2 = z^2$
|
|
| 39 |
$$
|
| 40 |
\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V
|
| 41 |
$$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
```
|
| 43 |
</Accordion>
|
| 44 |
|
|
|
|
| 1 |
+
import placeholder from '../../assets/image/placeholder.png';
|
| 2 |
+
import audioDemo from '../../assets/audio/audio-example.wav';
|
| 3 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro';
|
| 4 |
+
import Sidenote from '../../../components/Sidenote.astro';
|
| 5 |
+
import Wide from '../../../components/Wide.astro';
|
| 6 |
+
import Note from '../../../components/Note.astro';
|
| 7 |
+
import FullWidth from '../../../components/FullWidth.astro';
|
| 8 |
+
import Accordion from '../../../components/Accordion.astro';
|
| 9 |
+
import ResponsiveImage from '../../../components/ResponsiveImage.astro';
|
| 10 |
|
| 11 |
## Markdown
|
| 12 |
|
| 13 |
+
All the following **markdown features** are available **natively** in the `article.mdx` file. No imports needed, just write markdown directly:
|
| 14 |
+
|
| 15 |
+
**Text formatting** β `**Bold**` β **Bold**, `*italic*` β *italic*, `~~strikethrough~~` β ~~strikethrough~~
|
| 16 |
+
|
| 17 |
+
**Code** β `` `inline code` `` β `inline code`, triple backticks for code blocks
|
| 18 |
+
|
| 19 |
+
**Lists** β `- Item` for bullets, `1. Item` for numbered lists with nesting support
|
| 20 |
+
|
| 21 |
+
**Links** β `[text](url)` β [External links](https://example.com) and internal navigation
|
| 22 |
+
|
| 23 |
+
**Highlight** β `<mark>text</mark>` β <mark>Highlighted text</mark> for emphasis
|
| 24 |
+
|
| 25 |
+
See also the complete [**Markdown documentation**](https://www.markdownguide.org/basic-syntax/).
|
| 26 |
+
|
| 27 |
+
**Advanced features** β Explore specialized content types:
|
| 28 |
+
|
| 29 |
+
<div className="feature-grid">
|
| 30 |
+
<a href="#math" className="feature-card">
|
| 31 |
+
<strong>Math</strong>
|
| 32 |
+
<span>LaTeX equations</span>
|
| 33 |
+
</a>
|
| 34 |
+
<a href="#code-blocks" className="feature-card">
|
| 35 |
+
<strong>Code</strong>
|
| 36 |
+
<span>Syntax highlighting</span>
|
| 37 |
+
</a>
|
| 38 |
+
<a href="#citation-and-notes" className="feature-card">
|
| 39 |
+
<strong>Citation</strong>
|
| 40 |
+
<span>Academic references</span>
|
| 41 |
+
</a>
|
| 42 |
+
<a href="#footnote" className="feature-card">
|
| 43 |
+
<strong>Footnote</strong>
|
| 44 |
+
<span>Additional context</span>
|
| 45 |
+
</a>
|
| 46 |
+
<a href="#mermaid-diagrams" className="feature-card">
|
| 47 |
+
<strong>Mermaid</strong>
|
| 48 |
+
<span>Diagrams & flowcharts</span>
|
| 49 |
+
</a>
|
| 50 |
+
<a href="#separator" className="feature-card">
|
| 51 |
+
<strong>Separator</strong>
|
| 52 |
+
<span>Visual breaks</span>
|
| 53 |
+
</a>
|
| 54 |
+
<a href="#table" className="feature-card">
|
| 55 |
+
<strong>Table</strong>
|
| 56 |
+
<span>Data presentation</span>
|
| 57 |
+
</a>
|
| 58 |
+
<a href="#audio" className="feature-card">
|
| 59 |
+
<strong>Audio</strong>
|
| 60 |
+
<span>Sound embeds</span>
|
| 61 |
+
</a>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
### Math
|
|
|
|
| 69 |
\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V
|
| 70 |
$$
|
| 71 |
|
| 72 |
+
Aligned math equations are also supported. You can add IDs to equations using `\htmlId{my_id}` and then reference them with links like [this equation](#trajectory_definition).
|
| 73 |
+
|
| 74 |
+
```math
|
| 75 |
+
\htmlId{trajectory_definition}{\begin{align}
|
| 76 |
+
\log p_\theta(\mathcal D) &= \log \sum_{i=0}^N p_\theta ((o,a)_i) \\
|
| 77 |
+
&= \log \sum_{i=0}^N \int_{\text{supp}({Z})} p_\theta((o,a)_i \vert z) p(z) \\
|
| 78 |
+
&= \log \sum_{i=0}^N \int_{\text{supp}({Z})} \frac{q_\theta(z \vert (o,a)_i)}{q_\theta(z \vert (o,a)_i)} \cdot p_\theta((o,a)_i \vert z) p(z) \\
|
| 79 |
+
&= \log \sum_{i=0}^N \mathbb E_{z \sim p_\theta(\bullet \vert (o,a)_i)} [\frac{p(z)}{q_\theta(z \vert (o,a)_i)} \cdot p_\theta((o,a)_i \vert z)],
|
| 80 |
+
\end{align}}
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
|
| 84 |
<Accordion title="Code example">
|
| 85 |
```mdx
|
| 86 |
$x^2 + y^2 = z^2$
|
|
|
|
| 88 |
$$
|
| 89 |
\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V
|
| 90 |
$$
|
| 91 |
+
|
| 92 |
+
$$
|
| 93 |
+
\htmlId{trajectory_definition}{\begin{align}
|
| 94 |
+
\log p_\theta(\mathcal D) &= \log \sum_{i=0}^N p_\theta ((o,a)_i) \\
|
| 95 |
+
&= \log \sum_{i=0}^N \int_{\text{supp}({Z})} p_\theta((o,a)_i \vert z) p(z) \\
|
| 96 |
+
&= \log \sum_{i=0}^N \int_{\text{supp}({Z})} \frac{q_\theta(z \vert (o,a)_i)}{q_\theta(z \vert (o,a)_i)} \cdot p_\theta((o,a)_i \vert z) p(z) \\
|
| 97 |
+
&= \log \sum_{i=0}^N \mathbb E_{z \sim p_\theta(\bullet \vert (o,a)_i)} [\frac{p(z)}{q_\theta(z \vert (o,a)_i)} \cdot p_\theta((o,a)_i \vert z)],
|
| 98 |
+
\end{align}}
|
| 99 |
+
$$
|
| 100 |
+
|
| 101 |
```
|
| 102 |
</Accordion>
|
| 103 |
|
app/src/content/chapters/{vibe-coding-charts.mdx β demo/vibe-coding-charts.mdx}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
import HtmlEmbed from '
|
| 2 |
-
import Note from '
|
| 3 |
-
import TrackioWrapper from '
|
| 4 |
|
| 5 |
## Vibe coding charts
|
| 6 |
|
|
@@ -72,6 +72,6 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
|
|
| 72 |
---
|
| 73 |
<HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
|
| 74 |
|
| 75 |
-
### Trackio redesign experiment
|
| 76 |
-
<TrackioWrapper />
|
| 77 |
|
|
|
|
| 1 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro';
|
| 2 |
+
import Note from '../../../components/Note.astro';
|
| 3 |
+
import TrackioWrapper from '../../../components/TrackioWrapper.astro';
|
| 4 |
|
| 5 |
## Vibe coding charts
|
| 6 |
|
|
|
|
| 72 |
---
|
| 73 |
<HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
|
| 74 |
|
| 75 |
+
{/* ### Trackio redesign experiment
|
| 76 |
+
<TrackioWrapper /> */}
|
| 77 |
|
app/src/content/chapters/{writing-your-content.mdx β demo/writing-your-content.mdx}
RENAMED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
{/* IMPORTS */}
|
| 2 |
import { Image } from 'astro:assets';
|
| 3 |
-
import placeholder from '
|
| 4 |
-
import Sidenote from '
|
| 5 |
-
import Wide from '
|
| 6 |
-
import Note from '
|
| 7 |
-
import FullWidth from '
|
| 8 |
-
import HtmlEmbed from '
|
| 9 |
-
import ColorPicker from '
|
| 10 |
-
import Palettes from '
|
| 11 |
-
import audioDemo from '
|
| 12 |
-
import Accordion from '
|
| 13 |
|
| 14 |
## Writing your content
|
| 15 |
|
|
@@ -86,14 +86,15 @@ Your story. Write your content here.
|
|
| 86 |
|
| 87 |
<small className="muted">**Content** in app/src/content/article.mdx</small>
|
| 88 |
```mdx
|
| 89 |
-
import placeholder from '
|
| 90 |
-
import ResponsiveImage from '
|
| 91 |
-
import Sidenote from '
|
|
|
|
|
|
|
| 92 |
|
| 93 |
<Sidenote>
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
</Sidenote>
|
| 97 |
<ResponsiveImage src={placeholder} alt="Sample image with optimization" />
|
| 98 |
|
| 99 |
This paragraph is also written in Markdown.
|
|
@@ -131,13 +132,12 @@ Use the **color picker** below to see how the primary color affects the theme.
|
|
| 131 |
|
| 132 |
#### Brand color
|
| 133 |
|
|
|
|
|
|
|
| 134 |
<Sidenote>
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
Here is an example of an <a href="#">interactive element</a>.
|
| 140 |
-
</Fragment>
|
| 141 |
</Sidenote>
|
| 142 |
|
| 143 |
|
|
|
|
| 1 |
{/* IMPORTS */}
|
| 2 |
import { Image } from 'astro:assets';
|
| 3 |
+
import placeholder from '../../assets/image/placeholder.png';
|
| 4 |
+
import Sidenote from '../../../components/Sidenote.astro';
|
| 5 |
+
import Wide from '../../../components/Wide.astro';
|
| 6 |
+
import Note from '../../../components/Note.astro';
|
| 7 |
+
import FullWidth from '../../../components/FullWidth.astro';
|
| 8 |
+
import HtmlEmbed from '../../../components/HtmlEmbed.astro';
|
| 9 |
+
import ColorPicker from '../../../components/ColorPicker.astro';
|
| 10 |
+
import Palettes from '../../../components/Palettes.astro';
|
| 11 |
+
import audioDemo from '../../assets/audio/audio-example.wav';
|
| 12 |
+
import Accordion from '../../../components/Accordion.astro';
|
| 13 |
|
| 14 |
## Writing your content
|
| 15 |
|
|
|
|
| 86 |
|
| 87 |
<small className="muted">**Content** in app/src/content/article.mdx</small>
|
| 88 |
```mdx
|
| 89 |
+
import placeholder from '../../assets/image/placeholder.png'
|
| 90 |
+
import ResponsiveImage from '../../../components/ResponsiveImage.astro'
|
| 91 |
+
import Sidenote from '../../../components/Sidenote.astro'
|
| 92 |
+
|
| 93 |
+
This paragraph is written in Markdown.
|
| 94 |
|
| 95 |
<Sidenote>
|
| 96 |
+
A short callout inserted via a component.
|
| 97 |
+
</Sidenote>
|
|
|
|
| 98 |
<ResponsiveImage src={placeholder} alt="Sample image with optimization" />
|
| 99 |
|
| 100 |
This paragraph is also written in Markdown.
|
|
|
|
| 132 |
|
| 133 |
#### Brand color
|
| 134 |
|
| 135 |
+
<ColorPicker />
|
| 136 |
+
|
| 137 |
<Sidenote>
|
| 138 |
+
You can use the color picker to select the right color.
|
| 139 |
+
|
| 140 |
+
Here is an example of an <a href="#">interactive element</a>.
|
|
|
|
|
|
|
|
|
|
| 141 |
</Sidenote>
|
| 142 |
|
| 143 |
|
app/src/pages/index.astro
CHANGED
|
@@ -1,48 +1,57 @@
|
|
| 1 |
---
|
| 2 |
-
import * as ArticleMod from
|
| 3 |
|
| 4 |
-
import Hero from
|
| 5 |
-
import Footer from
|
| 6 |
-
import ThemeToggle from
|
| 7 |
-
import Seo from
|
| 8 |
-
import TableOfContents from
|
| 9 |
// Default OG image served from public/
|
| 10 |
-
const ogDefaultUrl =
|
| 11 |
-
import
|
| 12 |
-
import
|
| 13 |
const articleFM = (ArticleMod as any).frontmatter ?? {};
|
| 14 |
const Article = (ArticleMod as any).default;
|
| 15 |
-
const docTitle = articleFM?.title ??
|
| 16 |
// Allow explicit line breaks in the title via "\n" or YAML newlines
|
| 17 |
-
const docTitleHtml = (articleFM?.title ??
|
| 18 |
-
.replace(/\\n/g,
|
| 19 |
-
.replace(/\n/g,
|
| 20 |
-
const subtitle = articleFM?.subtitle ??
|
| 21 |
-
const description = articleFM?.description ??
|
| 22 |
// Accept authors as string[] or array of objects { name, url, affiliations? }
|
| 23 |
const rawAuthors = (articleFM as any)?.authors ?? [];
|
| 24 |
type Affiliation = { id: number; name: string; url?: string };
|
| 25 |
type Author = { name: string; url?: string; affiliationIndices?: number[] };
|
| 26 |
|
| 27 |
// Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? }
|
| 28 |
-
const rawAffils =
|
|
|
|
| 29 |
const normalizedAffiliations: Affiliation[] = (() => {
|
| 30 |
const seen: Map<string, number> = new Map();
|
| 31 |
const list: Affiliation[] = [];
|
| 32 |
const pushUnique = (name: string, url?: string) => {
|
| 33 |
-
const key = `${String(name).trim()}|${url ? String(url).trim() :
|
| 34 |
if (seen.has(key)) return seen.get(key)!;
|
| 35 |
const id = list.length + 1;
|
| 36 |
-
list.push({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
seen.set(key, id);
|
| 38 |
return id;
|
| 39 |
};
|
| 40 |
-
const input = Array.isArray(rawAffils)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
for (const a of input) {
|
| 42 |
-
if (typeof a ===
|
| 43 |
pushUnique(a);
|
| 44 |
-
} else if (a && typeof a ===
|
| 45 |
-
const name = a.name ?? a.label ?? a.text ?? a.affiliation ??
|
| 46 |
if (!String(name).trim()) continue;
|
| 47 |
const url = a.url || a.link;
|
| 48 |
// Respect provided numeric id for display stability if present and sequential; otherwise reassign
|
|
@@ -55,53 +64,69 @@ const normalizedAffiliations: Affiliation[] = (() => {
|
|
| 55 |
// Helper: ensure an affiliation exists and return its id
|
| 56 |
const ensureAffiliation = (val: any): number | undefined => {
|
| 57 |
if (val == null) return undefined;
|
| 58 |
-
if (typeof val ===
|
| 59 |
return Math.floor(val);
|
| 60 |
}
|
| 61 |
-
const name =
|
|
|
|
|
|
|
|
|
|
| 62 |
if (!name || !String(name).trim()) return undefined;
|
| 63 |
-
const existing = normalizedAffiliations.find(
|
|
|
|
|
|
|
| 64 |
if (existing) return existing.id;
|
| 65 |
const id = normalizedAffiliations.length + 1;
|
| 66 |
-
normalizedAffiliations.push({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return id;
|
| 68 |
};
|
| 69 |
|
| 70 |
// Normalize authors and map affiliations -> indices (Distill-like)
|
| 71 |
-
const normalizedAuthors: Author[] = (
|
|
|
|
|
|
|
| 72 |
.map((a: any) => {
|
| 73 |
-
if (typeof a ===
|
| 74 |
return { name: a } as Author;
|
| 75 |
}
|
| 76 |
-
const name = String(a?.name ||
|
| 77 |
const url = a?.url || a?.link;
|
| 78 |
let indices: number[] | undefined = undefined;
|
| 79 |
const raw = a?.affiliations ?? a?.affiliation ?? a?.affils;
|
| 80 |
if (raw != null) {
|
| 81 |
const entries = Array.isArray(raw) ? raw : [raw];
|
| 82 |
-
const ids = entries
|
|
|
|
|
|
|
| 83 |
const unique = Array.from(new Set(ids)).sort((x, y) => x - y);
|
| 84 |
if (unique.length) indices = unique;
|
| 85 |
}
|
| 86 |
return { name, url, affiliationIndices: indices } as Author;
|
| 87 |
})
|
| 88 |
.filter((a: Author) => a.name && a.name.trim().length > 0);
|
| 89 |
-
const authorNames: string[] = normalizedAuthors.map(a => a.name);
|
| 90 |
const published = articleFM?.published ?? undefined;
|
| 91 |
const tags = articleFM?.tags ?? [];
|
| 92 |
// Prefer seoThumbImage from frontmatter if provided
|
| 93 |
const fmOg = articleFM?.seoThumbImage as string | undefined;
|
| 94 |
-
const imageAbs: string =
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
// ---- Build citation text & BibTeX from frontmatter ----
|
| 99 |
-
const stripHtml = (text: string) => String(text ||
|
| 100 |
-
const rawTitle = articleFM?.title ??
|
| 101 |
const titleFlat = stripHtml(String(rawTitle))
|
| 102 |
-
.replace(/\\n/g,
|
| 103 |
-
.replace(/\n/g,
|
| 104 |
-
.replace(/\s+/g,
|
| 105 |
.trim();
|
| 106 |
const extractYear = (val: string | undefined): number | undefined => {
|
| 107 |
if (!val) return undefined;
|
|
@@ -112,105 +137,212 @@ const extractYear = (val: string | undefined): number | undefined => {
|
|
| 112 |
};
|
| 113 |
|
| 114 |
const year = extractYear(published);
|
| 115 |
-
const citationAuthorsText = authorNames.join(
|
| 116 |
-
const citationText = `${citationAuthorsText}${year ? ` (${year})` :
|
| 117 |
-
|
| 118 |
-
const authorsBib = authorNames.join(
|
| 119 |
-
const keyAuthor = (authorNames[0] ||
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
const envCollapse = false;
|
| 125 |
const tableOfContentAutoCollapse = Boolean(
|
| 126 |
-
(articleFM as any)?.tableOfContentAutoCollapse ??
|
|
|
|
|
|
|
| 127 |
);
|
| 128 |
// Licence note (HTML allowed)
|
| 129 |
-
const licence =
|
|
|
|
|
|
|
|
|
|
| 130 |
---
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
<head>
|
| 133 |
<meta charset="utf-8" />
|
| 134 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 135 |
-
<Seo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
<script is:inline>
|
| 137 |
(() => {
|
| 138 |
try {
|
| 139 |
-
const saved = localStorage.getItem(
|
| 140 |
-
const prefersDark =
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
} catch {}
|
| 144 |
})();
|
| 145 |
</script>
|
| 146 |
<script type="module" src="/scripts/color-palettes.js"></script>
|
| 147 |
-
|
| 148 |
<!-- TO MANAGE PROPERLY -->
|
| 149 |
-
<script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"
|
|
|
|
| 150 |
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
| 151 |
-
<script
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
</head>
|
| 154 |
<body>
|
| 155 |
<ThemeToggle />
|
| 156 |
-
<Hero
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
<section class="content-grid">
|
| 159 |
-
<TableOfContents
|
|
|
|
|
|
|
| 160 |
<main>
|
| 161 |
<Article />
|
| 162 |
</main>
|
| 163 |
</section>
|
| 164 |
|
| 165 |
-
<Footer
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
<script>
|
| 169 |
// Open external links in a new tab; keep internal anchors in-page
|
| 170 |
const setExternalTargets = () => {
|
| 171 |
const isExternal = (href) => {
|
| 172 |
-
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
};
|
| 174 |
-
document.querySelectorAll(
|
| 175 |
-
const href = a.getAttribute(
|
| 176 |
if (!href) return;
|
| 177 |
if (isExternal(href)) {
|
| 178 |
-
a.setAttribute(
|
| 179 |
-
a.setAttribute(
|
| 180 |
} else {
|
| 181 |
-
a.removeAttribute(
|
| 182 |
}
|
| 183 |
});
|
| 184 |
};
|
| 185 |
-
if (document.readyState ===
|
| 186 |
-
document.addEventListener(
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
</script>
|
| 189 |
|
| 190 |
<script>
|
| 191 |
// Delegate copy clicks for code blocks injected by rehypeCodeCopy
|
| 192 |
-
document.addEventListener(
|
| 193 |
const target = e.target instanceof Element ? e.target : null;
|
| 194 |
-
const btn = target ? target.closest(
|
| 195 |
if (!btn) return;
|
| 196 |
-
const card = btn.closest(
|
| 197 |
-
const pre = card && card.querySelector(
|
| 198 |
if (!pre) return;
|
| 199 |
-
const text = pre.textContent ||
|
| 200 |
try {
|
| 201 |
await navigator.clipboard.writeText(text.trim());
|
| 202 |
const old = btn.innerHTML;
|
| 203 |
-
btn.innerHTML =
|
|
|
|
| 204 |
setTimeout(() => (btn.innerHTML = old), 1200);
|
| 205 |
} catch {
|
| 206 |
-
btn.textContent =
|
| 207 |
-
setTimeout(() => (btn.textContent =
|
| 208 |
}
|
| 209 |
});
|
| 210 |
</script>
|
| 211 |
-
|
| 212 |
-
|
| 213 |
</body>
|
| 214 |
</html>
|
| 215 |
-
|
| 216 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
import * as ArticleMod from "../content/article.mdx";
|
| 3 |
|
| 4 |
+
import Hero from "../components/Hero.astro";
|
| 5 |
+
import Footer from "../components/Footer.astro";
|
| 6 |
+
import ThemeToggle from "../components/ThemeToggle.astro";
|
| 7 |
+
import Seo from "../components/Seo.astro";
|
| 8 |
+
import TableOfContents from "../components/TableOfContents.astro";
|
| 9 |
// Default OG image served from public/
|
| 10 |
+
const ogDefaultUrl = "/thumb.auto.jpg";
|
| 11 |
+
import "katex/dist/katex.min.css";
|
| 12 |
+
import "../styles/global.css";
|
| 13 |
const articleFM = (ArticleMod as any).frontmatter ?? {};
|
| 14 |
const Article = (ArticleMod as any).default;
|
| 15 |
+
const docTitle = articleFM?.title ?? "Untitled article";
|
| 16 |
// Allow explicit line breaks in the title via "\n" or YAML newlines
|
| 17 |
+
const docTitleHtml = (articleFM?.title ?? "Untitled article")
|
| 18 |
+
.replace(/\\n/g, "<br/>")
|
| 19 |
+
.replace(/\n/g, "<br/>");
|
| 20 |
+
const subtitle = articleFM?.subtitle ?? "";
|
| 21 |
+
const description = articleFM?.description ?? "";
|
| 22 |
// Accept authors as string[] or array of objects { name, url, affiliations? }
|
| 23 |
const rawAuthors = (articleFM as any)?.authors ?? [];
|
| 24 |
type Affiliation = { id: number; name: string; url?: string };
|
| 25 |
type Author = { name: string; url?: string; affiliationIndices?: number[] };
|
| 26 |
|
| 27 |
// Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? }
|
| 28 |
+
const rawAffils =
|
| 29 |
+
(articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? [];
|
| 30 |
const normalizedAffiliations: Affiliation[] = (() => {
|
| 31 |
const seen: Map<string, number> = new Map();
|
| 32 |
const list: Affiliation[] = [];
|
| 33 |
const pushUnique = (name: string, url?: string) => {
|
| 34 |
+
const key = `${String(name).trim()}|${url ? String(url).trim() : ""}`;
|
| 35 |
if (seen.has(key)) return seen.get(key)!;
|
| 36 |
const id = list.length + 1;
|
| 37 |
+
list.push({
|
| 38 |
+
id,
|
| 39 |
+
name: String(name).trim(),
|
| 40 |
+
url: url ? String(url) : undefined,
|
| 41 |
+
});
|
| 42 |
seen.set(key, id);
|
| 43 |
return id;
|
| 44 |
};
|
| 45 |
+
const input = Array.isArray(rawAffils)
|
| 46 |
+
? rawAffils
|
| 47 |
+
: rawAffils
|
| 48 |
+
? [rawAffils]
|
| 49 |
+
: [];
|
| 50 |
for (const a of input) {
|
| 51 |
+
if (typeof a === "string") {
|
| 52 |
pushUnique(a);
|
| 53 |
+
} else if (a && typeof a === "object") {
|
| 54 |
+
const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? "";
|
| 55 |
if (!String(name).trim()) continue;
|
| 56 |
const url = a.url || a.link;
|
| 57 |
// Respect provided numeric id for display stability if present and sequential; otherwise reassign
|
|
|
|
| 64 |
// Helper: ensure an affiliation exists and return its id
|
| 65 |
const ensureAffiliation = (val: any): number | undefined => {
|
| 66 |
if (val == null) return undefined;
|
| 67 |
+
if (typeof val === "number" && Number.isFinite(val) && val > 0) {
|
| 68 |
return Math.floor(val);
|
| 69 |
}
|
| 70 |
+
const name =
|
| 71 |
+
typeof val === "string"
|
| 72 |
+
? val
|
| 73 |
+
: (val?.name ?? val?.label ?? val?.text ?? val?.affiliation);
|
| 74 |
if (!name || !String(name).trim()) return undefined;
|
| 75 |
+
const existing = normalizedAffiliations.find(
|
| 76 |
+
(a) => a.name === String(name).trim(),
|
| 77 |
+
);
|
| 78 |
if (existing) return existing.id;
|
| 79 |
const id = normalizedAffiliations.length + 1;
|
| 80 |
+
normalizedAffiliations.push({
|
| 81 |
+
id,
|
| 82 |
+
name: String(name).trim(),
|
| 83 |
+
url: val?.url || val?.link,
|
| 84 |
+
});
|
| 85 |
return id;
|
| 86 |
};
|
| 87 |
|
| 88 |
// Normalize authors and map affiliations -> indices (Distill-like)
|
| 89 |
+
const normalizedAuthors: Author[] = (
|
| 90 |
+
Array.isArray(rawAuthors) ? rawAuthors : []
|
| 91 |
+
)
|
| 92 |
.map((a: any) => {
|
| 93 |
+
if (typeof a === "string") {
|
| 94 |
return { name: a } as Author;
|
| 95 |
}
|
| 96 |
+
const name = String(a?.name || "").trim();
|
| 97 |
const url = a?.url || a?.link;
|
| 98 |
let indices: number[] | undefined = undefined;
|
| 99 |
const raw = a?.affiliations ?? a?.affiliation ?? a?.affils;
|
| 100 |
if (raw != null) {
|
| 101 |
const entries = Array.isArray(raw) ? raw : [raw];
|
| 102 |
+
const ids = entries
|
| 103 |
+
.map(ensureAffiliation)
|
| 104 |
+
.filter((x): x is number => typeof x === "number");
|
| 105 |
const unique = Array.from(new Set(ids)).sort((x, y) => x - y);
|
| 106 |
if (unique.length) indices = unique;
|
| 107 |
}
|
| 108 |
return { name, url, affiliationIndices: indices } as Author;
|
| 109 |
})
|
| 110 |
.filter((a: Author) => a.name && a.name.trim().length > 0);
|
| 111 |
+
const authorNames: string[] = normalizedAuthors.map((a) => a.name);
|
| 112 |
const published = articleFM?.published ?? undefined;
|
| 113 |
const tags = articleFM?.tags ?? [];
|
| 114 |
// Prefer seoThumbImage from frontmatter if provided
|
| 115 |
const fmOg = articleFM?.seoThumbImage as string | undefined;
|
| 116 |
+
const imageAbs: string =
|
| 117 |
+
fmOg && fmOg.startsWith("http")
|
| 118 |
+
? fmOg
|
| 119 |
+
: Astro.site
|
| 120 |
+
? new URL(fmOg ?? ogDefaultUrl, Astro.site).toString()
|
| 121 |
+
: (fmOg ?? ogDefaultUrl);
|
| 122 |
|
| 123 |
// ---- Build citation text & BibTeX from frontmatter ----
|
| 124 |
+
const stripHtml = (text: string) => String(text || "").replace(/<[^>]*>/g, "");
|
| 125 |
+
const rawTitle = articleFM?.title ?? "Untitled article";
|
| 126 |
const titleFlat = stripHtml(String(rawTitle))
|
| 127 |
+
.replace(/\\n/g, " ")
|
| 128 |
+
.replace(/\n/g, " ")
|
| 129 |
+
.replace(/\s+/g, " ")
|
| 130 |
.trim();
|
| 131 |
const extractYear = (val: string | undefined): number | undefined => {
|
| 132 |
if (!val) return undefined;
|
|
|
|
| 137 |
};
|
| 138 |
|
| 139 |
const year = extractYear(published);
|
| 140 |
+
const citationAuthorsText = authorNames.join(", ");
|
| 141 |
+
const citationText = `${citationAuthorsText}${year ? ` (${year})` : ""}. "${titleFlat}".`;
|
| 142 |
+
|
| 143 |
+
const authorsBib = authorNames.join(" and ");
|
| 144 |
+
const keyAuthor = (authorNames[0] || "article")
|
| 145 |
+
.split(/\s+/)
|
| 146 |
+
.slice(-1)[0]
|
| 147 |
+
.toLowerCase();
|
| 148 |
+
const keyTitle = titleFlat
|
| 149 |
+
.toLowerCase()
|
| 150 |
+
.replace(/[^a-z0-9]+/g, "_")
|
| 151 |
+
.replace(/^_|_$/g, "")
|
| 152 |
+
.slice(0, 24);
|
| 153 |
+
const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`;
|
| 154 |
+
const doi = (ArticleMod as any)?.frontmatter?.doi
|
| 155 |
+
? String((ArticleMod as any).frontmatter.doi)
|
| 156 |
+
: undefined;
|
| 157 |
+
const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ""}${doi ? `doi={${doi}}` : ""}\n}`;
|
| 158 |
const envCollapse = false;
|
| 159 |
const tableOfContentAutoCollapse = Boolean(
|
| 160 |
+
(articleFM as any)?.tableOfContentAutoCollapse ??
|
| 161 |
+
(articleFM as any)?.tableOfContentsAutoCollapse ??
|
| 162 |
+
envCollapse,
|
| 163 |
);
|
| 164 |
// Licence note (HTML allowed)
|
| 165 |
+
const licence =
|
| 166 |
+
(articleFM as any)?.licence ??
|
| 167 |
+
(articleFM as any)?.license ??
|
| 168 |
+
(articleFM as any)?.licenseNote;
|
| 169 |
---
|
| 170 |
+
|
| 171 |
+
<html
|
| 172 |
+
lang="en"
|
| 173 |
+
data-theme="light"
|
| 174 |
+
data-toc-auto-collapse={tableOfContentAutoCollapse ? "1" : "0"}
|
| 175 |
+
>
|
| 176 |
<head>
|
| 177 |
<meta charset="utf-8" />
|
| 178 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 179 |
+
<Seo
|
| 180 |
+
title={docTitle}
|
| 181 |
+
description={description}
|
| 182 |
+
authors={authorNames}
|
| 183 |
+
published={published}
|
| 184 |
+
tags={tags}
|
| 185 |
+
image={imageAbs}
|
| 186 |
+
/>
|
| 187 |
<script is:inline>
|
| 188 |
(() => {
|
| 189 |
try {
|
| 190 |
+
const saved = localStorage.getItem("theme");
|
| 191 |
+
const prefersDark =
|
| 192 |
+
window.matchMedia &&
|
| 193 |
+
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
| 194 |
+
const theme = saved || (prefersDark ? "dark" : "light");
|
| 195 |
+
document.documentElement.setAttribute("data-theme", theme);
|
| 196 |
} catch {}
|
| 197 |
})();
|
| 198 |
</script>
|
| 199 |
<script type="module" src="/scripts/color-palettes.js"></script>
|
| 200 |
+
|
| 201 |
<!-- TO MANAGE PROPERLY -->
|
| 202 |
+
<script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"
|
| 203 |
+
></script>
|
| 204 |
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
| 205 |
+
<script
|
| 206 |
+
src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"
|
| 207 |
+
></script>
|
| 208 |
+
<script>
|
| 209 |
+
// Debug et initialisation globale du zoom
|
| 210 |
+
console.log("Global script: Starting zoom initialization");
|
| 211 |
+
|
| 212 |
+
function initializeZoom() {
|
| 213 |
+
console.log("Global script: Looking for zoomable images");
|
| 214 |
+
const zoomableImages = document.querySelectorAll(
|
| 215 |
+
'img[data-zoomable="1"]',
|
| 216 |
+
);
|
| 217 |
+
console.log("Global script: Found images:", zoomableImages.length);
|
| 218 |
+
|
| 219 |
+
if (window.mediumZoom && zoomableImages.length > 0) {
|
| 220 |
+
zoomableImages.forEach((img, index) => {
|
| 221 |
+
console.log(`Global script: Processing image ${index}:`, img);
|
| 222 |
+
|
| 223 |
+
// VΓ©rifier si dΓ©jΓ initialisΓ©
|
| 224 |
+
if (!img.classList.contains("medium-zoom-image")) {
|
| 225 |
+
try {
|
| 226 |
+
const instance = window.mediumZoom(img, {
|
| 227 |
+
background: "rgba(0,0,0,.85)",
|
| 228 |
+
margin: 24,
|
| 229 |
+
scrollOffset: 0,
|
| 230 |
+
});
|
| 231 |
+
console.log(
|
| 232 |
+
`Global script: Zoom initialized for image ${index}:`,
|
| 233 |
+
instance,
|
| 234 |
+
);
|
| 235 |
+
} catch (error) {
|
| 236 |
+
console.error(
|
| 237 |
+
`Global script: Error initializing zoom for image ${index}:`,
|
| 238 |
+
error,
|
| 239 |
+
);
|
| 240 |
+
}
|
| 241 |
+
} else {
|
| 242 |
+
console.log(`Global script: Image ${index} already has zoom`);
|
| 243 |
+
}
|
| 244 |
+
});
|
| 245 |
+
} else {
|
| 246 |
+
console.log(
|
| 247 |
+
"Global script: mediumZoom not available or no images found",
|
| 248 |
+
);
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Essayer d'initialiser immΓ©diatement
|
| 253 |
+
if (document.readyState === "loading") {
|
| 254 |
+
document.addEventListener("DOMContentLoaded", initializeZoom);
|
| 255 |
+
} else {
|
| 256 |
+
initializeZoom();
|
| 257 |
+
}
|
| 258 |
|
| 259 |
+
// Aussi essayer après le chargement complet
|
| 260 |
+
window.addEventListener("load", () => {
|
| 261 |
+
setTimeout(initializeZoom, 100);
|
| 262 |
+
});
|
| 263 |
+
</script>
|
| 264 |
</head>
|
| 265 |
<body>
|
| 266 |
<ThemeToggle />
|
| 267 |
+
<Hero
|
| 268 |
+
title={docTitleHtml}
|
| 269 |
+
titleRaw={docTitle}
|
| 270 |
+
description={subtitle}
|
| 271 |
+
authors={normalizedAuthors as any}
|
| 272 |
+
affiliations={normalizedAffiliations as any}
|
| 273 |
+
affiliation={articleFM?.affiliation}
|
| 274 |
+
published={articleFM?.published}
|
| 275 |
+
doi={doi}
|
| 276 |
+
/>
|
| 277 |
|
| 278 |
<section class="content-grid">
|
| 279 |
+
<TableOfContents
|
| 280 |
+
tableOfContentAutoCollapse={tableOfContentAutoCollapse}
|
| 281 |
+
/>
|
| 282 |
<main>
|
| 283 |
<Article />
|
| 284 |
</main>
|
| 285 |
</section>
|
| 286 |
|
| 287 |
+
<Footer
|
| 288 |
+
citationText={citationText}
|
| 289 |
+
bibtex={bibtex}
|
| 290 |
+
licence={licence}
|
| 291 |
+
doi={doi}
|
| 292 |
+
/>
|
| 293 |
|
| 294 |
<script>
|
| 295 |
// Open external links in a new tab; keep internal anchors in-page
|
| 296 |
const setExternalTargets = () => {
|
| 297 |
const isExternal = (href) => {
|
| 298 |
+
try {
|
| 299 |
+
const u = new URL(href, location.href);
|
| 300 |
+
return u.origin !== location.origin;
|
| 301 |
+
} catch {
|
| 302 |
+
return false;
|
| 303 |
+
}
|
| 304 |
};
|
| 305 |
+
document.querySelectorAll("a[href]").forEach((a) => {
|
| 306 |
+
const href = a.getAttribute("href");
|
| 307 |
if (!href) return;
|
| 308 |
if (isExternal(href)) {
|
| 309 |
+
a.setAttribute("target", "_blank");
|
| 310 |
+
a.setAttribute("rel", "noopener noreferrer");
|
| 311 |
} else {
|
| 312 |
+
a.removeAttribute("target");
|
| 313 |
}
|
| 314 |
});
|
| 315 |
};
|
| 316 |
+
if (document.readyState === "loading") {
|
| 317 |
+
document.addEventListener("DOMContentLoaded", setExternalTargets, {
|
| 318 |
+
once: true,
|
| 319 |
+
});
|
| 320 |
+
} else {
|
| 321 |
+
setExternalTargets();
|
| 322 |
+
}
|
| 323 |
</script>
|
| 324 |
|
| 325 |
<script>
|
| 326 |
// Delegate copy clicks for code blocks injected by rehypeCodeCopy
|
| 327 |
+
document.addEventListener("click", async (e) => {
|
| 328 |
const target = e.target instanceof Element ? e.target : null;
|
| 329 |
+
const btn = target ? target.closest(".code-copy") : null;
|
| 330 |
if (!btn) return;
|
| 331 |
+
const card = btn.closest(".code-card");
|
| 332 |
+
const pre = card && card.querySelector("pre");
|
| 333 |
if (!pre) return;
|
| 334 |
+
const text = pre.textContent || "";
|
| 335 |
try {
|
| 336 |
await navigator.clipboard.writeText(text.trim());
|
| 337 |
const old = btn.innerHTML;
|
| 338 |
+
btn.innerHTML =
|
| 339 |
+
'<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>';
|
| 340 |
setTimeout(() => (btn.innerHTML = old), 1200);
|
| 341 |
} catch {
|
| 342 |
+
btn.textContent = "Error";
|
| 343 |
+
setTimeout(() => (btn.textContent = "Copy"), 1200);
|
| 344 |
}
|
| 345 |
});
|
| 346 |
</script>
|
|
|
|
|
|
|
| 347 |
</body>
|
| 348 |
</html>
|
|
|
|
|
|
app/src/styles/_base.css
CHANGED
|
@@ -1,9 +1,17 @@
|
|
| 1 |
@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";
|
| 2 |
|
| 3 |
-
html {
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
.content-grid main {
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
.content-grid main h2 {
|
| 9 |
font-weight: 600;
|
|
@@ -29,25 +37,54 @@ html { font-size: 16px; line-height: 1.6; }
|
|
| 29 |
margin: var(--spacing-8) 0 var(--spacing-4);
|
| 30 |
}
|
| 31 |
|
| 32 |
-
.content-grid main a {
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
/* Do not underline heading links inside the article (not the TOC) */
|
| 36 |
.content-grid main h2 a,
|
| 37 |
.content-grid main h3 a,
|
| 38 |
.content-grid main h4 a,
|
| 39 |
.content-grid main h5 a,
|
| 40 |
-
.content-grid main h6 a {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
.content-grid main h2 a:hover,
|
| 42 |
.content-grid main h3 a:hover,
|
| 43 |
.content-grid main h4 a:hover,
|
| 44 |
.content-grid main h5 a:hover,
|
| 45 |
-
.content-grid main h6 a:hover {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
.content-grid main ul,
|
| 48 |
-
.content-grid main ol {
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
.content-grid main blockquote {
|
| 53 |
border-left: 2px solid var(--border-color);
|
|
@@ -57,7 +94,11 @@ html { font-size: 16px; line-height: 1.6; }
|
|
| 57 |
margin: var(--spacing-4) 0;
|
| 58 |
}
|
| 59 |
|
| 60 |
-
.content-grid main hr {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
.muted {
|
| 63 |
color: var(--muted-color);
|
|
@@ -65,4 +106,62 @@ html { font-size: 16px; line-height: 1.6; }
|
|
| 65 |
|
| 66 |
[data-footnote-ref] {
|
| 67 |
margin-left: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
|
|
|
| 1 |
@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";
|
| 2 |
|
| 3 |
+
html {
|
| 4 |
+
font-size: 16px;
|
| 5 |
+
line-height: 1.6;
|
| 6 |
+
}
|
| 7 |
|
| 8 |
+
.content-grid main {
|
| 9 |
+
color: var(--text-color);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.content-grid main p {
|
| 13 |
+
margin: 0 0 var(--spacing-3);
|
| 14 |
+
}
|
| 15 |
|
| 16 |
.content-grid main h2 {
|
| 17 |
font-weight: 600;
|
|
|
|
| 37 |
margin: var(--spacing-8) 0 var(--spacing-4);
|
| 38 |
}
|
| 39 |
|
| 40 |
+
.content-grid main a {
|
| 41 |
+
color: var(--primary-color);
|
| 42 |
+
text-decoration: none;
|
| 43 |
+
background: var(--sufrace-bg);
|
| 44 |
+
border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
|
| 45 |
+
;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.content-grid main a:hover {
|
| 49 |
+
color: var(--primary-color-hover);
|
| 50 |
+
border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
|
| 51 |
+
;
|
| 52 |
+
}
|
| 53 |
|
| 54 |
/* Do not underline heading links inside the article (not the TOC) */
|
| 55 |
.content-grid main h2 a,
|
| 56 |
.content-grid main h3 a,
|
| 57 |
.content-grid main h4 a,
|
| 58 |
.content-grid main h5 a,
|
| 59 |
+
.content-grid main h6 a {
|
| 60 |
+
color: inherit;
|
| 61 |
+
border-bottom: none;
|
| 62 |
+
text-decoration: none;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
.content-grid main h2 a:hover,
|
| 66 |
.content-grid main h3 a:hover,
|
| 67 |
.content-grid main h4 a:hover,
|
| 68 |
.content-grid main h5 a:hover,
|
| 69 |
+
.content-grid main h6 a:hover {
|
| 70 |
+
color: inherit;
|
| 71 |
+
border-bottom: none;
|
| 72 |
+
text-decoration: none;
|
| 73 |
+
}
|
| 74 |
|
| 75 |
.content-grid main ul,
|
| 76 |
+
.content-grid main ol {
|
| 77 |
+
padding-left: 24px;
|
| 78 |
+
margin: 0 0 var(--spacing-3);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.content-grid main li {
|
| 82 |
+
margin-bottom: var(--spacing-2);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.content-grid main li:last-child {
|
| 86 |
+
margin-bottom: 0;
|
| 87 |
+
}
|
| 88 |
|
| 89 |
.content-grid main blockquote {
|
| 90 |
border-left: 2px solid var(--border-color);
|
|
|
|
| 94 |
margin: var(--spacing-4) 0;
|
| 95 |
}
|
| 96 |
|
| 97 |
+
.content-grid main hr {
|
| 98 |
+
border: none;
|
| 99 |
+
border-bottom: 1px solid var(--border-color);
|
| 100 |
+
margin: var(--spacing-5) 0;
|
| 101 |
+
}
|
| 102 |
|
| 103 |
.muted {
|
| 104 |
color: var(--muted-color);
|
|
|
|
| 106 |
|
| 107 |
[data-footnote-ref] {
|
| 108 |
margin-left: 4px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.content-grid main mark {
|
| 112 |
+
background-color: color-mix(in srgb, var(--primary-color, #007AFF) 10%, transparent);
|
| 113 |
+
border: 1px solid color-mix(in srgb, var(--primary-color) 18%, transparent);
|
| 114 |
+
color: inherit;
|
| 115 |
+
padding: 4px 6px;
|
| 116 |
+
border-radius: 4px;
|
| 117 |
+
font-weight: 500;
|
| 118 |
+
box-decoration-break: clone;
|
| 119 |
+
-webkit-box-decoration-break: clone;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.feature-grid {
|
| 123 |
+
display: grid;
|
| 124 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 125 |
+
gap: 12px;
|
| 126 |
+
margin: 46px 0;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.feature-card {
|
| 130 |
+
display: flex;
|
| 131 |
+
flex-direction: column;
|
| 132 |
+
padding: 16px;
|
| 133 |
+
border: 1px solid color-mix(in srgb, var(--primary-color) 40%, transparent);
|
| 134 |
+
;
|
| 135 |
+
background: color-mix(in srgb, var(--primary-color, #007AFF) 05%, transparent) !important;
|
| 136 |
+
border-radius: 8px;
|
| 137 |
+
text-decoration: none;
|
| 138 |
+
color: inherit;
|
| 139 |
+
transition: all 0.2s ease;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.feature-card:hover {
|
| 143 |
+
transform: translateY(-2px);
|
| 144 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.feature-card strong {
|
| 148 |
+
font-size: 14px;
|
| 149 |
+
font-weight: 600;
|
| 150 |
+
color: var(--text-color);
|
| 151 |
+
color: var(--primary-color) !important;
|
| 152 |
+
margin-bottom: 0px !important;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.feature-card span {
|
| 156 |
+
font-size: 12px;
|
| 157 |
+
color: var(--muted-color);
|
| 158 |
+
color: var(--primary-color) !important;
|
| 159 |
+
margin-bottom: 0px !important;
|
| 160 |
+
opacity: 1;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.katex .tag {
|
| 164 |
+
background: none;
|
| 165 |
+
border: none;
|
| 166 |
+
opacity: 0.4;
|
| 167 |
}
|
app/src/styles/_variables.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
--neutral-300: rgb(228, 228, 228);
|
| 9 |
--neutral-200: rgb(245, 245, 245);
|
| 10 |
|
| 11 |
-
--default-font-family: Source Sans Pro,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
| 12 |
|
| 13 |
/* Brand (OKLCH base + derived states) */
|
| 14 |
--primary-base: oklch(0.75 0.12 340);
|
|
@@ -19,10 +19,10 @@
|
|
| 19 |
|
| 20 |
/* Text & Surfaces */
|
| 21 |
--page-bg: #ffffff;
|
| 22 |
-
--text-color: rgba(0,0,0
|
| 23 |
-
--transparent-page-contrast: rgba(255,255,255
|
| 24 |
-
--muted-color: rgba(0,0,0
|
| 25 |
-
--border-color: rgba(0,0,0
|
| 26 |
--surface-bg: #fafafa;
|
| 27 |
--code-bg: #f6f8fa;
|
| 28 |
|
|
@@ -52,8 +52,10 @@
|
|
| 52 |
@custom-media --bp-content-collapse (max-width: 1100px);
|
| 53 |
|
| 54 |
/* Layout */
|
| 55 |
-
--content-padding-x: 16px;
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
|
| 58 |
/* Config */
|
| 59 |
--palette-count: 8;
|
|
@@ -86,7 +88,7 @@
|
|
| 86 |
/* Charts (global) */
|
| 87 |
--axis-color: var(--muted-color);
|
| 88 |
--tick-color: var(--text-color);
|
| 89 |
-
--grid-color: rgba(0,0,0
|
| 90 |
}
|
| 91 |
|
| 92 |
/* ============================================================================ */
|
|
@@ -94,17 +96,17 @@
|
|
| 94 |
/* ============================================================================ */
|
| 95 |
[data-theme="dark"] {
|
| 96 |
--page-bg: #0f1115;
|
| 97 |
-
--text-color: rgba(255,255,255
|
| 98 |
-
--muted-color: rgba(255,255,255
|
| 99 |
-
--border-color: rgba(255,255,255
|
| 100 |
--surface-bg: #12151b;
|
| 101 |
--code-bg: #12151b;
|
| 102 |
-
--transparent-page-contrast: rgba(0,0,0
|
| 103 |
-
|
| 104 |
/* Charts (global) */
|
| 105 |
--axis-color: var(--muted-color);
|
| 106 |
--tick-color: var(--muted-color);
|
| 107 |
-
--grid-color: rgba(255,255,255
|
| 108 |
|
| 109 |
/* Primary (lower L in dark) */
|
| 110 |
--primary-color: oklch(from var(--primary-base) calc(l - 0.08) c h);
|
|
|
|
| 8 |
--neutral-300: rgb(228, 228, 228);
|
| 9 |
--neutral-200: rgb(245, 245, 245);
|
| 10 |
|
| 11 |
+
--default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
| 12 |
|
| 13 |
/* Brand (OKLCH base + derived states) */
|
| 14 |
--primary-base: oklch(0.75 0.12 340);
|
|
|
|
| 19 |
|
| 20 |
/* Text & Surfaces */
|
| 21 |
--page-bg: #ffffff;
|
| 22 |
+
--text-color: rgba(0, 0, 0, .85);
|
| 23 |
+
--transparent-page-contrast: rgba(255, 255, 255, .85);
|
| 24 |
+
--muted-color: rgba(0, 0, 0, .6);
|
| 25 |
+
--border-color: rgba(0, 0, 0, .1);
|
| 26 |
--surface-bg: #fafafa;
|
| 27 |
--code-bg: #f6f8fa;
|
| 28 |
|
|
|
|
| 52 |
@custom-media --bp-content-collapse (max-width: 1100px);
|
| 53 |
|
| 54 |
/* Layout */
|
| 55 |
+
--content-padding-x: 16px;
|
| 56 |
+
/* default page gutter */
|
| 57 |
+
--block-spacing-y: var(--spacing-4);
|
| 58 |
+
/* default vertical spacing between block components */
|
| 59 |
|
| 60 |
/* Config */
|
| 61 |
--palette-count: 8;
|
|
|
|
| 88 |
/* Charts (global) */
|
| 89 |
--axis-color: var(--muted-color);
|
| 90 |
--tick-color: var(--text-color);
|
| 91 |
+
--grid-color: rgba(0, 0, 0, .08);
|
| 92 |
}
|
| 93 |
|
| 94 |
/* ============================================================================ */
|
|
|
|
| 96 |
/* ============================================================================ */
|
| 97 |
[data-theme="dark"] {
|
| 98 |
--page-bg: #0f1115;
|
| 99 |
+
--text-color: rgba(255, 255, 255, .9);
|
| 100 |
+
--muted-color: rgba(255, 255, 255, .7);
|
| 101 |
+
--border-color: rgba(255, 255, 255, .15);
|
| 102 |
--surface-bg: #12151b;
|
| 103 |
--code-bg: #12151b;
|
| 104 |
+
--transparent-page-contrast: rgba(0, 0, 0, .85);
|
| 105 |
+
|
| 106 |
/* Charts (global) */
|
| 107 |
--axis-color: var(--muted-color);
|
| 108 |
--tick-color: var(--muted-color);
|
| 109 |
+
--grid-color: rgba(255, 255, 255, .10);
|
| 110 |
|
| 111 |
/* Primary (lower L in dark) */
|
| 112 |
--primary-color: oklch(from var(--primary-base) calc(l - 0.08) c h);
|