Spaces:
Running
Running
thibaud frere
commited on
Commit
·
b2ac38d
1
Parent(s):
e329457
update
Browse files- README.md +1 -1
- app/.astro/astro/content.d.ts +169 -0
- app/scripts/export-pdf.mjs +2 -16
- app/src/components/Header.astro +4 -3
- app/src/content/article.mdx +21 -19
- app/src/pages/index.astro +4 -7
- app/src/styles/_base.scss +1 -1
- app/src/styles/_layout.scss +2 -2
- app/src/styles/components/_code.scss +1 -1
- app/src/styles/components/_footer.scss +1 -1
- fragments/plotly/banner.py +26 -26
- fragments/plotly/line.py +2 -2
- fragments/plotly/pyproject.toml +1 -1
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: '
|
| 3 |
emoji: 📝
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
|
|
| 1 |
---
|
| 2 |
+
title: 'Research article template'
|
| 3 |
emoji: 📝
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
app/.astro/astro/content.d.ts
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
type DataEntryMap = {
|
| 158 |
+
"fragments": Record<string, {
|
| 159 |
+
id: string;
|
| 160 |
+
collection: "fragments";
|
| 161 |
+
data: any;
|
| 162 |
+
}>;
|
| 163 |
+
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
| 167 |
+
|
| 168 |
+
export type ContentConfig = never;
|
| 169 |
+
}
|
app/scripts/export-pdf.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
import { spawn } from 'node:child_process';
|
| 3 |
import { setTimeout as delay } from 'node:timers/promises';
|
| 4 |
import { chromium } from 'playwright';
|
| 5 |
-
import { resolve
|
| 6 |
import { promises as fs } from 'node:fs';
|
| 7 |
import process from 'node:process';
|
| 8 |
|
|
@@ -180,26 +180,12 @@ async function main() {
|
|
| 180 |
});
|
| 181 |
console.log(`✅ PDF generated: ${outPath}`);
|
| 182 |
|
| 183 |
-
//
|
| 184 |
-
const distCompatPath = resolve(cwd, 'dist', 'article.pdf');
|
| 185 |
-
try {
|
| 186 |
-
if (basename(outPath) !== 'article.pdf') {
|
| 187 |
-
await fs.copyFile(outPath, distCompatPath);
|
| 188 |
-
console.log(`✅ PDF copied (dist compat): ${distCompatPath}`);
|
| 189 |
-
}
|
| 190 |
-
} catch (e) {
|
| 191 |
-
console.warn('Unable to copy PDF to dist/article.pdf:', e?.message || e);
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
// Also copy into public under 2 names: slug.pdf and article.pdf (compat)
|
| 195 |
const publicSlugPath = resolve(cwd, 'public', `${outFileBase}.pdf`);
|
| 196 |
-
const publicCompatPath = resolve(cwd, 'public', 'article.pdf');
|
| 197 |
try {
|
| 198 |
await fs.mkdir(resolve(cwd, 'public'), { recursive: true });
|
| 199 |
await fs.copyFile(outPath, publicSlugPath);
|
| 200 |
-
await fs.copyFile(outPath, publicCompatPath);
|
| 201 |
console.log(`✅ PDF copied to: ${publicSlugPath}`);
|
| 202 |
-
console.log(`✅ PDF copied (compat): ${publicCompatPath}`);
|
| 203 |
} catch (e) {
|
| 204 |
console.warn('Unable to copy PDF to public/:', e?.message || e);
|
| 205 |
}
|
|
|
|
| 2 |
import { spawn } from 'node:child_process';
|
| 3 |
import { setTimeout as delay } from 'node:timers/promises';
|
| 4 |
import { chromium } from 'playwright';
|
| 5 |
+
import { resolve } from 'node:path';
|
| 6 |
import { promises as fs } from 'node:fs';
|
| 7 |
import process from 'node:process';
|
| 8 |
|
|
|
|
| 180 |
});
|
| 181 |
console.log(`✅ PDF generated: ${outPath}`);
|
| 182 |
|
| 183 |
+
// Copy into public only under the slugified name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
const publicSlugPath = resolve(cwd, 'public', `${outFileBase}.pdf`);
|
|
|
|
| 185 |
try {
|
| 186 |
await fs.mkdir(resolve(cwd, 'public'), { recursive: true });
|
| 187 |
await fs.copyFile(outPath, publicSlugPath);
|
|
|
|
| 188 |
console.log(`✅ PDF copied to: ${publicSlugPath}`);
|
|
|
|
| 189 |
} catch (e) {
|
| 190 |
console.warn('Unable to copy PDF to public/:', e?.message || e);
|
| 191 |
}
|
app/src/components/Header.astro
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
---
|
|
|
|
|
|
|
| 2 |
interface Props {
|
| 3 |
title: string;
|
| 4 |
description?: string;
|
|
@@ -6,11 +8,10 @@ interface Props {
|
|
| 6 |
const { title, description } = Astro.props as Props;
|
| 7 |
---
|
| 8 |
<section class="hero">
|
| 9 |
-
<h1 class="hero-title"
|
| 10 |
<div class="hero-banner">
|
| 11 |
-
<
|
| 12 |
{description && <p class="hero-desc">{description}</p>}
|
| 13 |
</div>
|
| 14 |
</section>
|
| 15 |
|
| 16 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
import HtmlFragment from "./HtmlFragment.astro";
|
| 3 |
+
|
| 4 |
interface Props {
|
| 5 |
title: string;
|
| 6 |
description?: string;
|
|
|
|
| 8 |
const { title, description } = Astro.props as Props;
|
| 9 |
---
|
| 10 |
<section class="hero">
|
| 11 |
+
<h1 class="hero-title" set:html={title}></h1>
|
| 12 |
<div class="hero-banner">
|
| 13 |
+
<HtmlFragment src="banner.html" />
|
| 14 |
{description && <p class="hero-desc">{description}</p>}
|
| 15 |
</div>
|
| 16 |
</section>
|
| 17 |
|
|
|
app/src/content/article.mdx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
---
|
| 2 |
title: "From Idea to Interactive:\n A Modern Template for Scientific Writing
|
| 3 |
"
|
|
|
|
| 4 |
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
| 5 |
authors:
|
| 6 |
- "John Doe"
|
|
@@ -40,7 +41,7 @@ import visualPoster from "../assets/images/visual-vocabulary-poster.png";
|
|
| 40 |
|
| 41 |
This template is inspired by [**Distill**](https://distill.pub); we aim to preserve the best of it while modernizing the stack. Their work is highly inspiring.
|
| 42 |
|
| 43 |
-
|
| 44 |
|
| 45 |
<div className="tag-list">
|
| 46 |
<span className="tag">Markdown based</span>
|
|
@@ -94,7 +95,7 @@ The easiest way to get online is to clone [this Hugging Face Space](https://hugg
|
|
| 94 |
|
| 95 |
### Large files (Git LFS)
|
| 96 |
|
| 97 |
-
Track binaries (e.g., `.png`, `.wav`) with Git LFS to keep the repository lean. This project is preconfigured to store such files via LFS
|
| 98 |
|
| 99 |
|
| 100 |
## Writing Your Content
|
|
@@ -103,15 +104,16 @@ Track binaries (e.g., `.png`, `.wav`) with Git LFS to keep the repository lean.
|
|
| 103 |
|
| 104 |
Your article lives in two places:
|
| 105 |
|
| 106 |
-
- `app/src/content/` — where you can find the article.mdx
|
| 107 |
- `app/src/assets/` — images, audio, and other static assets. (handled by git lfs)
|
| 108 |
|
| 109 |
-
The initial skeleton of an article looks like this:
|
| 110 |
|
| 111 |
```mdx
|
| 112 |
{/* HEADER */}
|
| 113 |
---
|
| 114 |
-
title: "
|
|
|
|
| 115 |
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
| 116 |
authors:
|
| 117 |
- "John Doe"
|
|
@@ -139,7 +141,7 @@ This is a short paragraph written in Markdown. Below is an example image:
|
|
| 139 |
|
| 140 |
|
| 141 |
|
| 142 |
-
**Available
|
| 143 |
|
| 144 |
<div className="tag-list">
|
| 145 |
<a className="tag" href="#math">Math</a>
|
|
@@ -177,7 +179,7 @@ $$
|
|
| 177 |
|
| 178 |
### Images
|
| 179 |
|
| 180 |
-
Responsive images automatically generate an 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
|
| 181 |
|
| 182 |
**Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
|
| 183 |
|
|
@@ -209,7 +211,7 @@ import myImage from '../assets/images/placeholder.jpg'
|
|
| 209 |
<Image src={myImage} data-zoomable alt="Example with caption and credit" loading="lazy" />
|
| 210 |
<figcaption>
|
| 211 |
Optimized image with a descriptive caption.
|
| 212 |
-
<span className="image-credit">
|
| 213 |
</figcaption>
|
| 214 |
</figure>
|
| 215 |
```
|
|
@@ -243,11 +245,11 @@ greet("Astro")
|
|
| 243 |
|
| 244 |
Here are a few variations using the same bibliography:
|
| 245 |
|
| 246 |
-
1) In-text citation with brackets: [@example2023].
|
| 247 |
|
| 248 |
-
2) Narrative citation
|
| 249 |
|
| 250 |
-
3) Multiple citations and a footnote together: see [@vaswani2017attention; @example2023] for related work. Also note this footnote[^f1].
|
| 251 |
|
| 252 |
[^f1]: Footnote attached to the sentence above.
|
| 253 |
|
|
@@ -265,9 +267,9 @@ Here are a few variations using the same bibliography:
|
|
| 265 |
### Asides
|
| 266 |
|
| 267 |
<Aside>
|
| 268 |
-
This paragraph presents a key idea concisely.
|
| 269 |
<Fragment slot="aside">
|
| 270 |
-
Side note for brief context or a definition.
|
| 271 |
</Fragment>
|
| 272 |
</Aside>
|
| 273 |
|
|
@@ -348,7 +350,7 @@ import audioDemo from '../assets/audio/audio-example.wav'
|
|
| 348 |
|
| 349 |
#### Html Fragments
|
| 350 |
|
| 351 |
-
The main purpose of the ```HtmlFragment``` component is to embed a
|
| 352 |
|
| 353 |
<div className="plot-card">
|
| 354 |
<HtmlFragment src="line.html" />
|
|
@@ -362,7 +364,7 @@ import HtmlFragment from '../components/HtmlFragment.astro'
|
|
| 362 |
|
| 363 |
#### Iframes
|
| 364 |
|
| 365 |
-
You can embed external content in your article using **iframes**. For example, TrackIO
|
| 366 |
|
| 367 |
<iframe className="plot-card" src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
|
| 368 |
|
|
@@ -407,14 +409,14 @@ Choosing colors well is critical: a **palette** encodes **meaning** (categories,
|
|
| 407 |
|
| 408 |
### Use the right chart
|
| 409 |
|
| 410 |
-
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
|
| 411 |
|
| 412 |
<figure>
|
| 413 |
-
<a href=
|
| 414 |
<Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
|
| 415 |
</a>
|
| 416 |
<figcaption>
|
| 417 |
-
|
| 418 |
— <a href="https://ft-interactive.github.io/visual-vocabulary/" target="_blank" rel="noopener noreferrer">Website</a>
|
| 419 |
</figcaption>
|
| 420 |
</figure>
|
|
@@ -422,5 +424,5 @@ Picking the right visualization depends on your goal (compare values, show distr
|
|
| 422 |
|
| 423 |
## Conclusions
|
| 424 |
|
| 425 |
-
This template provides a practical baseline for writing and sharing technical articles with math
|
| 426 |
|
|
|
|
| 1 |
---
|
| 2 |
title: "From Idea to Interactive:\n A Modern Template for Scientific Writing
|
| 3 |
"
|
| 4 |
+
subtitle: "It's nice to have a cute interactive banner"
|
| 5 |
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
| 6 |
authors:
|
| 7 |
- "John Doe"
|
|
|
|
| 41 |
|
| 42 |
This template is inspired by [**Distill**](https://distill.pub); we aim to preserve the best of it while modernizing the stack. Their work is highly inspiring.
|
| 43 |
|
| 44 |
+
#### Features
|
| 45 |
|
| 46 |
<div className="tag-list">
|
| 47 |
<span className="tag">Markdown based</span>
|
|
|
|
| 95 |
|
| 96 |
### Large files (Git LFS)
|
| 97 |
|
| 98 |
+
**Track binaries** (e.g., `.png`, `.wav`) with **Git LFS** to keep the repository lean. This project is preconfigured to store such files via **LFS**.
|
| 99 |
|
| 100 |
|
| 101 |
## Writing Your Content
|
|
|
|
| 104 |
|
| 105 |
Your article lives in two places:
|
| 106 |
|
| 107 |
+
- `app/src/content/` — where you can find the article.mdx, bibliography.bib and html fragments.
|
| 108 |
- `app/src/assets/` — images, audio, and other static assets. (handled by git lfs)
|
| 109 |
|
| 110 |
+
The **initial skeleton** of an article looks like this:
|
| 111 |
|
| 112 |
```mdx
|
| 113 |
{/* HEADER */}
|
| 114 |
---
|
| 115 |
+
title: "This is the main title"
|
| 116 |
+
subtitle: "This will be displayed just below the banner"
|
| 117 |
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
| 118 |
authors:
|
| 119 |
- "John Doe"
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
|
| 144 |
+
**Available blocks**:
|
| 145 |
|
| 146 |
<div className="tag-list">
|
| 147 |
<a className="tag" href="#math">Math</a>
|
|
|
|
| 179 |
|
| 180 |
### Images
|
| 181 |
|
| 182 |
+
**Responsive images** automatically generate an 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**.
|
| 183 |
|
| 184 |
**Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
|
| 185 |
|
|
|
|
| 211 |
<Image src={myImage} data-zoomable alt="Example with caption and credit" loading="lazy" />
|
| 212 |
<figcaption>
|
| 213 |
Optimized image with a descriptive caption.
|
| 214 |
+
<span className="image-credit">Credit: Photo by <a href="https://example.com">Author</a></span>
|
| 215 |
</figcaption>
|
| 216 |
</figure>
|
| 217 |
```
|
|
|
|
| 245 |
|
| 246 |
Here are a few variations using the same bibliography:
|
| 247 |
|
| 248 |
+
1) **In-text citation** with brackets: [@example2023].
|
| 249 |
|
| 250 |
+
2) **Narrative citation**: As shown by @vaswani2017attention, transformers enable efficient sequence modeling.
|
| 251 |
|
| 252 |
+
3) **Multiple citations** and a **footnote** together: see [@vaswani2017attention; @example2023] for related work. Also note this footnote[^f1].
|
| 253 |
|
| 254 |
[^f1]: Footnote attached to the sentence above.
|
| 255 |
|
|
|
|
| 267 |
### Asides
|
| 268 |
|
| 269 |
<Aside>
|
| 270 |
+
This paragraph presents a **key idea** concisely.
|
| 271 |
<Fragment slot="aside">
|
| 272 |
+
**Side note** for brief context or a definition.
|
| 273 |
</Fragment>
|
| 274 |
</Aside>
|
| 275 |
|
|
|
|
| 350 |
|
| 351 |
#### Html Fragments
|
| 352 |
|
| 353 |
+
The main purpose of the ```HtmlFragment``` component is to embed a **Plotly** or **D3.js** chart in your article. Libraries are already imported in the template.
|
| 354 |
|
| 355 |
<div className="plot-card">
|
| 356 |
<HtmlFragment src="line.html" />
|
|
|
|
| 364 |
|
| 365 |
#### Iframes
|
| 366 |
|
| 367 |
+
You can embed external content in your article using **iframes**. For example, **TrackIO**—a lightweight dashboard to monitor machine learning experiments—can be used this way. The example below opens a demo project and displays a couple of metrics. You can customize the **query parameters** (`project`, `metrics`, `sidebar`, etc.) to fit your needs.
|
| 368 |
|
| 369 |
<iframe className="plot-card" src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
|
| 370 |
|
|
|
|
| 409 |
|
| 410 |
### Use the right chart
|
| 411 |
|
| 412 |
+
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**.
|
| 413 |
|
| 414 |
<figure>
|
| 415 |
+
<a href={visualPoster.src} target="_blank" rel="noopener noreferrer">
|
| 416 |
<Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
|
| 417 |
</a>
|
| 418 |
<figcaption>
|
| 419 |
+
A handy reference to select chart types by purpose. Click to enlarge.
|
| 420 |
— <a href="https://ft-interactive.github.io/visual-vocabulary/" target="_blank" rel="noopener noreferrer">Website</a>
|
| 421 |
</figcaption>
|
| 422 |
</figure>
|
|
|
|
| 424 |
|
| 425 |
## Conclusions
|
| 426 |
|
| 427 |
+
This template provides a **practical baseline** for writing and sharing technical articles with **math**, **citations**, and **interactive figures**. **Start simple**, **iterate** on structure and style, and keep content **maintainable** for future readers and collaborators.
|
| 428 |
|
app/src/pages/index.astro
CHANGED
|
@@ -3,6 +3,7 @@ import Article, { frontmatter as articleFM } from '../content/article.mdx';
|
|
| 3 |
import Meta from '../components/Meta.astro';
|
| 4 |
import HtmlFragment from '../components/HtmlFragment.astro';
|
| 5 |
import Footer from '../components/Footer.astro';
|
|
|
|
| 6 |
import ThemeToggle from '../components/ThemeToggle.astro';
|
| 7 |
import SeoHead from '../components/SeoHead.astro';
|
| 8 |
import ogDefault from '../assets/images/visual-vocabulary-poster.png';
|
|
@@ -13,6 +14,7 @@ const docTitle = articleFM?.title ?? 'Untitled article';
|
|
| 13 |
const docTitleHtml = (articleFM?.title ?? 'Untitled article')
|
| 14 |
.replace(/\\n/g, '<br/>')
|
| 15 |
.replace(/\n/g, '<br/>');
|
|
|
|
| 16 |
const description = articleFM?.description ?? '';
|
| 17 |
const authors = articleFM?.authors ?? [];
|
| 18 |
const published = articleFM?.published ?? undefined;
|
|
@@ -71,13 +73,8 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
|
|
| 71 |
</head>
|
| 72 |
<body>
|
| 73 |
<ThemeToggle />
|
| 74 |
-
<
|
| 75 |
-
|
| 76 |
-
<div class="hero-banner">
|
| 77 |
-
<HtmlFragment src="banner.html" />
|
| 78 |
-
<p class="hero-desc">It's nice to have a cute interactive banner!</p>
|
| 79 |
-
</div>
|
| 80 |
-
</section>
|
| 81 |
|
| 82 |
<section class="article-header">
|
| 83 |
<Meta title={docTitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} />
|
|
|
|
| 3 |
import Meta from '../components/Meta.astro';
|
| 4 |
import HtmlFragment from '../components/HtmlFragment.astro';
|
| 5 |
import Footer from '../components/Footer.astro';
|
| 6 |
+
import Header from '../components/Header.astro';
|
| 7 |
import ThemeToggle from '../components/ThemeToggle.astro';
|
| 8 |
import SeoHead from '../components/SeoHead.astro';
|
| 9 |
import ogDefault from '../assets/images/visual-vocabulary-poster.png';
|
|
|
|
| 14 |
const docTitleHtml = (articleFM?.title ?? 'Untitled article')
|
| 15 |
.replace(/\\n/g, '<br/>')
|
| 16 |
.replace(/\n/g, '<br/>');
|
| 17 |
+
const subtitle = articleFM?.subtitle ?? '';
|
| 18 |
const description = articleFM?.description ?? '';
|
| 19 |
const authors = articleFM?.authors ?? [];
|
| 20 |
const published = articleFM?.published ?? undefined;
|
|
|
|
| 73 |
</head>
|
| 74 |
<body>
|
| 75 |
<ThemeToggle />
|
| 76 |
+
<Header title={docTitleHtml} description={subtitle}>
|
| 77 |
+
</Header>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
<section class="article-header">
|
| 80 |
<Meta title={docTitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} />
|
app/src/styles/_base.scss
CHANGED
|
@@ -186,7 +186,7 @@ figcaption { text-align: center; font-size: 0.9rem; color: var(--muted-color); m
|
|
| 186 |
.image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
| 187 |
|
| 188 |
// ============================================================================
|
| 189 |
-
// Buttons (minimal,
|
| 190 |
// ============================================================================
|
| 191 |
.meta .meta-container-cell button {
|
| 192 |
appearance: none;
|
|
|
|
| 186 |
.image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
| 187 |
|
| 188 |
// ============================================================================
|
| 189 |
+
// Buttons (minimal, clean)
|
| 190 |
// ============================================================================
|
| 191 |
.meta .meta-container-cell button {
|
| 192 |
appearance: none;
|
app/src/styles/_layout.scss
CHANGED
|
@@ -22,9 +22,9 @@ main > nav:first-of-type { display: none; }
|
|
| 22 |
|
| 23 |
// Mobile TOC accordion
|
| 24 |
.toc-mobile { display: none; margin: 8px 0 16px; }
|
| 25 |
-
.toc-mobile > summary { cursor: pointer; list-style: none; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 8px;
|
| 26 |
.toc-mobile[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
| 27 |
-
.toc-mobile nav { border-left: none; padding: 10px 12px; font-size: 14px; border: 1px solid var(--border-color); border-top: none; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;
|
| 28 |
.toc-mobile nav ul { margin: 0 0 6px; padding-left: 1em; }
|
| 29 |
.toc-mobile nav li { list-style: none; margin: .25em 0; }
|
| 30 |
.toc-mobile nav a { color: var(--text-color); text-decoration: none; border-bottom: none; }
|
|
|
|
| 22 |
|
| 23 |
// Mobile TOC accordion
|
| 24 |
.toc-mobile { display: none; margin: 8px 0 16px; }
|
| 25 |
+
.toc-mobile > summary { cursor: pointer; list-style: none; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-color); font-weight: 600; }
|
| 26 |
.toc-mobile[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
| 27 |
+
.toc-mobile nav { border-left: none; padding: 10px 12px; font-size: 14px; border: 1px solid var(--border-color); border-top: none; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; }
|
| 28 |
.toc-mobile nav ul { margin: 0 0 6px; padding-left: 1em; }
|
| 29 |
.toc-mobile nav li { list-style: none; margin: .25em 0; }
|
| 30 |
.toc-mobile nav a { color: var(--text-color); text-decoration: none; border-bottom: none; }
|
app/src/styles/components/_code.scss
CHANGED
|
@@ -9,7 +9,7 @@ section.content-grid pre { overflow-x: auto; width: 100%; max-width: 100%; box-s
|
|
| 9 |
section.content-grid pre code { display: inline-block; min-width: 100%; }
|
| 10 |
|
| 11 |
/* Wrap long lines on mobile to avoid overflow (URLs, etc.) */
|
| 12 |
-
@media (max-width:
|
| 13 |
.astro-code,
|
| 14 |
section.content-grid pre { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
|
| 15 |
section.content-grid pre code { white-space: pre-wrap; display: block; min-width: 0; }
|
|
|
|
| 9 |
section.content-grid pre code { display: inline-block; min-width: 100%; }
|
| 10 |
|
| 11 |
/* Wrap long lines on mobile to avoid overflow (URLs, etc.) */
|
| 12 |
+
@media (max-width: 1100px) {
|
| 13 |
.astro-code,
|
| 14 |
section.content-grid pre { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
|
| 15 |
section.content-grid pre code { white-space: pre-wrap; display: block; min-width: 0; }
|
app/src/styles/components/_footer.scss
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
.distill-footer { contain: layout style; font-size: 0.8em; line-height: 1.7em; margin-top: 60px; margin-bottom: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); color: rgba(0, 0, 0, 0.5); }
|
| 2 |
.footer-inner { max-width: 1280px; margin: 0 auto; padding: 60px 16px 48px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
|
| 3 |
|
| 4 |
-
//
|
| 5 |
.citation-block,
|
| 6 |
.references-block { display: contents; }
|
| 7 |
.citation-block > h3,
|
|
|
|
| 1 |
.distill-footer { contain: layout style; font-size: 0.8em; line-height: 1.7em; margin-top: 60px; margin-bottom: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); color: rgba(0, 0, 0, 0.5); }
|
| 2 |
.footer-inner { max-width: 1280px; margin: 0 auto; padding: 60px 16px 48px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
|
| 3 |
|
| 4 |
+
// Use the parent grid (3 columns like .content-grid)
|
| 5 |
.citation-block,
|
| 6 |
.references-block { display: contents; }
|
| 7 |
.citation-block > h3,
|
fragments/plotly/banner.py
CHANGED
|
@@ -2,62 +2,62 @@ import plotly.graph_objects as go
|
|
| 2 |
import numpy as np
|
| 3 |
import pandas as pd
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
cx, cy = 1.5, 0.5 #
|
| 7 |
-
a, b = 1.3, 0.45 #
|
| 8 |
-
|
| 9 |
-
#
|
| 10 |
-
num_points = 3000 #
|
| 11 |
-
num_arms = 3 #
|
| 12 |
-
num_turns = 2.1 #
|
| 13 |
-
angle_jitter = 0.12 #
|
| 14 |
-
pos_noise = 0.015 #
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
t = np.random.rand(num_points) * (2 * np.pi * num_turns) # progression
|
| 18 |
arm_indices = np.random.randint(0, num_arms, size=num_points)
|
| 19 |
arm_offsets = arm_indices * (2 * np.pi / num_arms)
|
| 20 |
|
| 21 |
theta = t + arm_offsets + np.random.randn(num_points) * angle_jitter
|
| 22 |
|
| 23 |
-
#
|
| 24 |
r_norm = (t / (2 * np.pi * num_turns)) ** 0.9
|
| 25 |
|
| 26 |
-
#
|
| 27 |
noise_x = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
|
| 28 |
noise_y = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
|
| 29 |
|
| 30 |
-
#
|
| 31 |
x_spiral = cx + a * r_norm * np.cos(theta) + noise_x
|
| 32 |
y_spiral = cy + b * r_norm * np.sin(theta) + noise_y
|
| 33 |
|
| 34 |
-
#
|
| 35 |
bulge_points = int(0.18 * num_points)
|
| 36 |
phi_b = 2 * np.pi * np.random.rand(bulge_points)
|
| 37 |
-
r_b = (np.random.rand(bulge_points) ** 2.2) * 0.22 #
|
| 38 |
noise_x_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
|
| 39 |
noise_y_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
|
| 40 |
x_bulge = cx + a * r_b * np.cos(phi_b) + noise_x_b
|
| 41 |
y_bulge = cy + b * r_b * np.sin(phi_b) + noise_y_b
|
| 42 |
|
| 43 |
-
#
|
| 44 |
x = np.concatenate([x_spiral, x_bulge])
|
| 45 |
y = np.concatenate([y_spiral, y_bulge])
|
| 46 |
|
| 47 |
-
#
|
| 48 |
z_spiral = 1 - r_norm
|
| 49 |
-
z_bulge = 1 - (r_b / max(r_b.max(), 1e-6)) #
|
| 50 |
z_raw = np.concatenate([z_spiral, z_bulge])
|
| 51 |
|
| 52 |
-
#
|
| 53 |
sizes = (z_raw + 1) * 5
|
| 54 |
|
| 55 |
-
#
|
| 56 |
|
| 57 |
df = pd.DataFrame({
|
| 58 |
"x": x,
|
| 59 |
"y": y,
|
| 60 |
-
"z": sizes, #
|
| 61 |
})
|
| 62 |
|
| 63 |
def get_label(z):
|
|
@@ -70,10 +70,10 @@ def get_label(z):
|
|
| 70 |
else:
|
| 71 |
return "biiig dot"
|
| 72 |
|
| 73 |
-
# Labels
|
| 74 |
df["label"] = pd.Series(z_raw).apply(get_label)
|
| 75 |
|
| 76 |
-
#
|
| 77 |
df = df.sort_values(by="z", ascending=True).reset_index(drop=True)
|
| 78 |
|
| 79 |
fig = go.Figure()
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
import pandas as pd
|
| 4 |
|
| 5 |
+
# Scene parameters (same ranges as the Astro integration)
|
| 6 |
+
cx, cy = 1.5, 0.5 # center
|
| 7 |
+
a, b = 1.3, 0.45 # max extent in x/y (ellipse for anisotropy)
|
| 8 |
+
|
| 9 |
+
# Spiral galaxy parameters
|
| 10 |
+
num_points = 3000 # more dots
|
| 11 |
+
num_arms = 3 # number of spiral arms
|
| 12 |
+
num_turns = 2.1 # number of turns per arm
|
| 13 |
+
angle_jitter = 0.12 # angular jitter to fan out the arms
|
| 14 |
+
pos_noise = 0.015 # global position noise
|
| 15 |
+
|
| 16 |
+
# Generate points along spiral arms (Archimedean spiral)
|
| 17 |
+
t = np.random.rand(num_points) * (2 * np.pi * num_turns) # progression along the arm
|
| 18 |
arm_indices = np.random.randint(0, num_arms, size=num_points)
|
| 19 |
arm_offsets = arm_indices * (2 * np.pi / num_arms)
|
| 20 |
|
| 21 |
theta = t + arm_offsets + np.random.randn(num_points) * angle_jitter
|
| 22 |
|
| 23 |
+
# Normalized radius (0->center, 1->edge). Power <1 to densify the core
|
| 24 |
r_norm = (t / (2 * np.pi * num_turns)) ** 0.9
|
| 25 |
|
| 26 |
+
# Radial/lateral noise that slightly increases with radius
|
| 27 |
noise_x = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
|
| 28 |
noise_y = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
|
| 29 |
|
| 30 |
+
# Elliptic projection
|
| 31 |
x_spiral = cx + a * r_norm * np.cos(theta) + noise_x
|
| 32 |
y_spiral = cy + b * r_norm * np.sin(theta) + noise_y
|
| 33 |
|
| 34 |
+
# Central bulge (additional points very close to the core)
|
| 35 |
bulge_points = int(0.18 * num_points)
|
| 36 |
phi_b = 2 * np.pi * np.random.rand(bulge_points)
|
| 37 |
+
r_b = (np.random.rand(bulge_points) ** 2.2) * 0.22 # compact bulge
|
| 38 |
noise_x_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
|
| 39 |
noise_y_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
|
| 40 |
x_bulge = cx + a * r_b * np.cos(phi_b) + noise_x_b
|
| 41 |
y_bulge = cy + b * r_b * np.sin(phi_b) + noise_y_b
|
| 42 |
|
| 43 |
+
# Concatenation
|
| 44 |
x = np.concatenate([x_spiral, x_bulge])
|
| 45 |
y = np.concatenate([y_spiral, y_bulge])
|
| 46 |
|
| 47 |
+
# Central intensity (for sizes/colors). 1 at center, ~0 at edge
|
| 48 |
z_spiral = 1 - r_norm
|
| 49 |
+
z_bulge = 1 - (r_b / max(r_b.max(), 1e-6)) # very bright bulge
|
| 50 |
z_raw = np.concatenate([z_spiral, z_bulge])
|
| 51 |
|
| 52 |
+
# Sizes: keep the 5..10 scale for consistency
|
| 53 |
sizes = (z_raw + 1) * 5
|
| 54 |
|
| 55 |
+
# Remove intermediate filtering: keep all placed points, filter at the very end
|
| 56 |
|
| 57 |
df = pd.DataFrame({
|
| 58 |
"x": x,
|
| 59 |
"y": y,
|
| 60 |
+
"z": sizes, # reused for size+color as before
|
| 61 |
})
|
| 62 |
|
| 63 |
def get_label(z):
|
|
|
|
| 70 |
else:
|
| 71 |
return "biiig dot"
|
| 72 |
|
| 73 |
+
# Labels based on central intensity
|
| 74 |
df["label"] = pd.Series(z_raw).apply(get_label)
|
| 75 |
|
| 76 |
+
# Rendering order: small points first, big ones after (on top)
|
| 77 |
df = df.sort_values(by="z", ascending=True).reset_index(drop=True)
|
| 78 |
|
| 79 |
fig = go.Figure()
|
fragments/plotly/line.py
CHANGED
|
@@ -120,11 +120,11 @@ fig.update_layout(
|
|
| 120 |
),
|
| 121 |
)
|
| 122 |
|
| 123 |
-
#
|
| 124 |
output_path = os.path.join(os.path.dirname(__file__), "fragments", "line.html")
|
| 125 |
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 126 |
|
| 127 |
-
#
|
| 128 |
post_script = """
|
| 129 |
(function(){
|
| 130 |
function attach(gd){
|
|
|
|
| 120 |
),
|
| 121 |
)
|
| 122 |
|
| 123 |
+
# Write the fragment next to this file into src/fragments/line.html (robust path)
|
| 124 |
output_path = os.path.join(os.path.dirname(__file__), "fragments", "line.html")
|
| 125 |
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 126 |
|
| 127 |
+
# Inject a small post-render script to round the hover box corners
|
| 128 |
post_script = """
|
| 129 |
(function(){
|
| 130 |
function attach(gd){
|
fragments/plotly/pyproject.toml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
[tool.poetry]
|
| 2 |
name = "blogpost-fine-tasks-python"
|
| 3 |
version = "0.1.0"
|
| 4 |
-
description = "
|
| 5 |
package-mode = false
|
| 6 |
|
| 7 |
[tool.poetry.dependencies]
|
|
|
|
| 1 |
[tool.poetry]
|
| 2 |
name = "blogpost-fine-tasks-python"
|
| 3 |
version = "0.1.0"
|
| 4 |
+
description = "Plotly fragment generation scripts and HTML/Markdown conversions for the blogpost."
|
| 5 |
package-mode = false
|
| 6 |
|
| 7 |
[tool.poetry.dependencies]
|