Spaces:
Running
Running
thibaud frere
commited on
Commit
·
c24ea90
1
Parent(s):
b9e7b9b
update
Browse files- README.md +0 -1
- app/.astro/astro/content.d.ts +0 -185
- app/astro.config.mjs +0 -2
- app/src/assets/{images → icones}/moon.svg +0 -0
- app/src/assets/{images → icones}/sun.svg +0 -0
- app/src/components/Note.astro +7 -6
- app/src/components/ThemeToggle.astro +2 -2
- app/src/content/article.mdx +11 -46
- app/src/content/chapters/{writing-you-content.mdx → available-blocks.mdx} +6 -83
- app/src/content/chapters/best-pratices.mdx +24 -4
- app/src/content/chapters/getting-started.mdx +42 -0
- app/src/content/chapters/writing-your-content.mdx +116 -0
- app/src/content/fragments/banner.html +2 -2
- app/src/content/fragments/color-picker.html +35 -22
- app/src/content/fragments/d3-bar.html +6 -5
- app/src/content/fragments/d3-line.html +9 -9
- app/src/content/fragments/heatmap.html +1 -1
- app/src/content/fragments/palettes.html +84 -305
- app/src/pages/index.astro +51 -11
- app/src/styles/_base.css +27 -3
- app/src/styles/_variables.css +6 -6
- app/src/styles/components/_poltly.css +2 -2
README.md
CHANGED
|
@@ -12,7 +12,6 @@ thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jp
|
|
| 12 |
|
| 13 |
TO DO :
|
| 14 |
|
| 15 |
-
- fix banner pour l'export pdf et la thumb
|
| 16 |
- rename le titre ?
|
| 17 |
- Vérifier la biliographie comment elle marche
|
| 18 |
|
|
|
|
| 12 |
|
| 13 |
TO DO :
|
| 14 |
|
|
|
|
| 15 |
- rename le titre ?
|
| 16 |
- Vérifier la biliographie comment elle marche
|
| 17 |
|
app/.astro/astro/content.d.ts
CHANGED
|
@@ -1,185 +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 |
-
"best-pratices.mdx": {
|
| 156 |
-
id: "best-pratices.mdx";
|
| 157 |
-
slug: "best-pratices";
|
| 158 |
-
body: string;
|
| 159 |
-
collection: "chapters";
|
| 160 |
-
data: any
|
| 161 |
-
} & { render(): Render[".mdx"] };
|
| 162 |
-
"writing-you-content.mdx": {
|
| 163 |
-
id: "writing-you-content.mdx";
|
| 164 |
-
slug: "writing-you-content";
|
| 165 |
-
body: string;
|
| 166 |
-
collection: "chapters";
|
| 167 |
-
data: any
|
| 168 |
-
} & { render(): Render[".mdx"] };
|
| 169 |
-
};
|
| 170 |
-
|
| 171 |
-
};
|
| 172 |
-
|
| 173 |
-
type DataEntryMap = {
|
| 174 |
-
"fragments": Record<string, {
|
| 175 |
-
id: string;
|
| 176 |
-
collection: "fragments";
|
| 177 |
-
data: any;
|
| 178 |
-
}>;
|
| 179 |
-
|
| 180 |
-
};
|
| 181 |
-
|
| 182 |
-
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
| 183 |
-
|
| 184 |
-
export type ContentConfig = never;
|
| 185 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/astro.config.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import mdx from '@astrojs/mdx';
|
|
| 3 |
import mermaid from 'astro-mermaid';
|
| 4 |
import remarkMath from 'remark-math';
|
| 5 |
import rehypeKatex from 'rehype-katex';
|
| 6 |
-
import remarkToc from 'remark-toc';
|
| 7 |
import remarkFootnotes from 'remark-footnotes';
|
| 8 |
import rehypeSlug from 'rehype-slug';
|
| 9 |
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
@@ -30,7 +29,6 @@ export default defineConfig({
|
|
| 30 |
}
|
| 31 |
},
|
| 32 |
remarkPlugins: [
|
| 33 |
-
[remarkToc, { heading: 'Table of Contents', maxDepth: 3 }],
|
| 34 |
remarkMath,
|
| 35 |
[remarkFootnotes, { inlineNotes: true }]
|
| 36 |
],
|
|
|
|
| 3 |
import mermaid from 'astro-mermaid';
|
| 4 |
import remarkMath from 'remark-math';
|
| 5 |
import rehypeKatex from 'rehype-katex';
|
|
|
|
| 6 |
import remarkFootnotes from 'remark-footnotes';
|
| 7 |
import rehypeSlug from 'rehype-slug';
|
| 8 |
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
|
|
| 29 |
}
|
| 30 |
},
|
| 31 |
remarkPlugins: [
|
|
|
|
| 32 |
remarkMath,
|
| 33 |
[remarkFootnotes, { inlineNotes: true }]
|
| 34 |
],
|
app/src/assets/{images → icones}/moon.svg
RENAMED
|
File without changes
|
app/src/assets/{images → icones}/sun.svg
RENAMED
|
File without changes
|
app/src/components/Note.astro
CHANGED
|
@@ -1,23 +1,24 @@
|
|
| 1 |
---
|
| 2 |
-
const { title
|
| 3 |
const wrapperClass = ["note", className].filter(Boolean).join(" ");
|
|
|
|
| 4 |
---
|
| 5 |
<div class={wrapperClass} {...props}>
|
| 6 |
-
<div class="note__header">
|
| 7 |
-
<span class="note__emoji">{emoji}</span>
|
| 8 |
{title && <span class="note__title">{title}</span>}
|
| 9 |
-
</div>
|
| 10 |
<div class="note__content">
|
| 11 |
<slot />
|
| 12 |
</div>
|
| 13 |
</div>
|
| 14 |
|
| 15 |
<style>
|
| 16 |
-
.note { background: var(--surface-bg); border-left: 2px solid
|
| 17 |
.note__header { display: flex; align-items: center; gap: 6px; font-weight: 600; color: var(--text-color); margin-bottom: 6px; }
|
| 18 |
.note__emoji { font-size: 24px; line-height: 1; }
|
| 19 |
.note__title { font-size: 13px; letter-spacing: .2px; }
|
| 20 |
.note__content { color: var(--text-color); font-size: 0.95rem; }
|
|
|
|
| 21 |
</style>
|
| 22 |
|
| 23 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
const { title, emoji, class: className, ...props } = Astro.props;
|
| 3 |
const wrapperClass = ["note", className].filter(Boolean).join(" ");
|
| 4 |
+
const hasHeader = (emoji && String(emoji).length > 0) || (title && String(title).length > 0);
|
| 5 |
---
|
| 6 |
<div class={wrapperClass} {...props}>
|
| 7 |
+
{hasHeader && <div class="note__header">
|
| 8 |
+
{emoji && <span class="note__emoji">{emoji}</span>}
|
| 9 |
{title && <span class="note__title">{title}</span>}
|
| 10 |
+
</div>}
|
| 11 |
<div class="note__content">
|
| 12 |
<slot />
|
| 13 |
</div>
|
| 14 |
</div>
|
| 15 |
|
| 16 |
<style>
|
| 17 |
+
.note { background: var(--surface-bg); border-left: 2px solid var(--border-color); border-radius: 4px; padding: 10px 14px; margin: 12px 0; }
|
| 18 |
.note__header { display: flex; align-items: center; gap: 6px; font-weight: 600; color: var(--text-color); margin-bottom: 6px; }
|
| 19 |
.note__emoji { font-size: 24px; line-height: 1; }
|
| 20 |
.note__title { font-size: 13px; letter-spacing: .2px; }
|
| 21 |
.note__content { color: var(--text-color); font-size: 0.95rem; }
|
| 22 |
+
.note__content > p:last-of-type { margin-bottom: 0 !important; }
|
| 23 |
</style>
|
| 24 |
|
|
|
app/src/components/ThemeToggle.astro
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
import sunIconUrl from "../assets/
|
| 3 |
-
import moonIconUrl from "../assets/
|
| 4 |
---
|
| 5 |
<button id="theme-toggle" aria-label="Toggle color theme">
|
| 6 |
<img class="icon light" src={sunIconUrl} alt="light" width="20" height="20" />
|
|
|
|
| 1 |
---
|
| 2 |
+
import sunIconUrl from "../assets/icones/sun.svg?url";
|
| 3 |
+
import moonIconUrl from "../assets/icones/moon.svg?url";
|
| 4 |
---
|
| 5 |
<button id="theme-toggle" aria-label="Toggle color theme">
|
| 6 |
<img class="icon light" src={sunIconUrl} alt="light" width="20" height="20" />
|
app/src/content/article.mdx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: "From Idea to
|
| 3 |
"
|
| 4 |
subtitle: "Markdown-first research article template with math, citations, and interactive figures."
|
| 5 |
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
|
@@ -23,8 +23,11 @@ import placeholder from "../assets/images/placeholder.png";
|
|
| 23 |
import audioDemo from "../assets/audio/audio-example.wav";
|
| 24 |
import Aside from "../components/Aside.astro";
|
| 25 |
import visualPoster from "../assets/images/visual-vocabulary-poster.png";
|
|
|
|
| 26 |
import BestPractices from "./chapters/best-pratices.mdx";
|
| 27 |
-
import WritingYourContent from "./chapters/writing-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
<Aside>
|
| 30 |
Welcome to this single-page research article template built with **Markdown**.
|
|
@@ -86,7 +89,7 @@ Not every contribution fits a PDF. Treat demos, visualizations, and interactive
|
|
| 86 |
This project is heavely inspired by [**Distill**](https://distill.pub) (2016–2021), which championed clear, web‑native scholarship.
|
| 87 |
|
| 88 |
|
| 89 |
-
### Notable examples of excellent scientific articles
|
| 90 |
|
| 91 |
A short, curated list of well‑designed and often interactive work:
|
| 92 |
|
|
@@ -96,56 +99,18 @@ A short, curated list of well‑designed and often interactive work:
|
|
| 96 |
- **ConvNetJS — Neural networks in the browser**: [cs.stanford.edu/people/karpathy/convnetjs](http://cs.stanford.edu/people/karpathy/convnetjs/)
|
| 97 |
- **Explorable Explanations — Collection**: [explorableexplanations.com](http://explorableexplanations.com/)
|
| 98 |
- **Distill — Why Momentum Really Works**: [distill.pub/2017/momentum](https://distill.pub/2017/momentum/)
|
|
|
|
| 99 |
|
| 100 |
-
|
| 101 |
-
## Getting Started
|
| 102 |
-
|
| 103 |
-
### Installation
|
| 104 |
-
|
| 105 |
-
<Aside>
|
| 106 |
-
```bash
|
| 107 |
-
git lfs install
|
| 108 |
-
git lfs pull
|
| 109 |
-
cd app
|
| 110 |
-
npm install
|
| 111 |
-
```
|
| 112 |
-
<Fragment slot="aside">
|
| 113 |
-
You can use yarn alternatively to npm.
|
| 114 |
-
</Fragment>
|
| 115 |
-
</Aside>
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
### Development
|
| 119 |
-
|
| 120 |
-
```bash
|
| 121 |
-
npm run dev
|
| 122 |
-
```
|
| 123 |
-
|
| 124 |
-
### Build
|
| 125 |
-
|
| 126 |
-
```bash
|
| 127 |
-
npm run build
|
| 128 |
-
```
|
| 129 |
-
|
| 130 |
-
Serving the `dist/` directory on any static host is enough to deliver the site.
|
| 131 |
-
|
| 132 |
-
A [slug-title].pdf and thumb.jpg are also generated at build time. You can find them in the public folder.
|
| 133 |
-
|
| 134 |
-
### Deploy
|
| 135 |
-
|
| 136 |
-
The easiest way to get online is to clone [this Hugging Face Space](https://huggingface.co/spaces/tfrere/science-blog-template) and push your changes; every push triggers an automatic build and deploy.
|
| 137 |
-
|
| 138 |
-
### Large files (Git LFS)
|
| 139 |
-
|
| 140 |
-
**Track binaries** (e.g., `.png`, `.wav`) with **Git LFS** to keep the repository lean. This project is preconfigured to store such files via **LFS**.
|
| 141 |
-
|
| 142 |
|
| 143 |
<WritingYourContent />
|
| 144 |
|
|
|
|
|
|
|
| 145 |
<BestPractices />
|
| 146 |
|
| 147 |
|
| 148 |
-
##
|
| 149 |
|
| 150 |
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.
|
| 151 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "From Idea to Life:\n A Modern Template for Scientific Writing
|
| 3 |
"
|
| 4 |
subtitle: "Markdown-first research article template with math, citations, and interactive figures."
|
| 5 |
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
|
|
|
| 23 |
import audioDemo from "../assets/audio/audio-example.wav";
|
| 24 |
import Aside from "../components/Aside.astro";
|
| 25 |
import visualPoster from "../assets/images/visual-vocabulary-poster.png";
|
| 26 |
+
|
| 27 |
import BestPractices from "./chapters/best-pratices.mdx";
|
| 28 |
+
import WritingYourContent from "./chapters/writing-your-content.mdx";
|
| 29 |
+
import AvailableBlocks from "./chapters/available-blocks.mdx";
|
| 30 |
+
import GettingStarted from "./chapters/getting-started.mdx";
|
| 31 |
|
| 32 |
<Aside>
|
| 33 |
Welcome to this single-page research article template built with **Markdown**.
|
|
|
|
| 89 |
This project is heavely inspired by [**Distill**](https://distill.pub) (2016–2021), which championed clear, web‑native scholarship.
|
| 90 |
|
| 91 |
|
| 92 |
+
{/* ### Notable examples of excellent scientific articles
|
| 93 |
|
| 94 |
A short, curated list of well‑designed and often interactive work:
|
| 95 |
|
|
|
|
| 99 |
- **ConvNetJS — Neural networks in the browser**: [cs.stanford.edu/people/karpathy/convnetjs](http://cs.stanford.edu/people/karpathy/convnetjs/)
|
| 100 |
- **Explorable Explanations — Collection**: [explorableexplanations.com](http://explorableexplanations.com/)
|
| 101 |
- **Distill — Why Momentum Really Works**: [distill.pub/2017/momentum](https://distill.pub/2017/momentum/)
|
| 102 |
+
*/}
|
| 103 |
|
| 104 |
+
<GettingStarted />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
<WritingYourContent />
|
| 107 |
|
| 108 |
+
<AvailableBlocks />
|
| 109 |
+
|
| 110 |
<BestPractices />
|
| 111 |
|
| 112 |
|
| 113 |
+
## Conclusion
|
| 114 |
|
| 115 |
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.
|
| 116 |
|
app/src/content/chapters/{writing-you-content.mdx → available-blocks.mdx}
RENAMED
|
@@ -1,87 +1,12 @@
|
|
| 1 |
-
{/* IMPORTS */}
|
| 2 |
import { Image } from 'astro:assets';
|
| 3 |
import placeholder from '../../assets/images/placeholder.png';
|
|
|
|
|
|
|
| 4 |
import Aside from '../../components/Aside.astro';
|
| 5 |
import Wide from '../../components/Wide.astro';
|
| 6 |
import FullBleed from '../../components/FullBleed.astro';
|
| 7 |
-
import HtmlFragment from '../../components/HtmlFragment.astro';
|
| 8 |
-
import audioDemo from '../../assets/audio/audio-example.wav';
|
| 9 |
-
|
| 10 |
-
## Writing Your Content
|
| 11 |
-
|
| 12 |
-
### Introduction
|
| 13 |
-
|
| 14 |
-
Your article lives in two places:
|
| 15 |
-
|
| 16 |
-
- `app/src/content/` — where you can find the article.mdx, bibliography.bib and html fragments.
|
| 17 |
-
- `app/src/assets/` — images, audio, and other static assets. (handled by git lfs)
|
| 18 |
-
|
| 19 |
-
This is MDX, its basically a markdown file with html and astro components.
|
| 20 |
-
The **initial skeleton** of an article looks like this.
|
| 21 |
-
|
| 22 |
-
```mdx
|
| 23 |
-
{/* HEADER */}
|
| 24 |
-
---
|
| 25 |
-
title: "This is the main title"
|
| 26 |
-
subtitle: "This will be displayed just below the banner"
|
| 27 |
-
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
| 28 |
-
authors:
|
| 29 |
-
- "John Doe"
|
| 30 |
-
- "Alice Martin"
|
| 31 |
-
- "Robert Brown"
|
| 32 |
-
affiliation: "Hugging Face"
|
| 33 |
-
published: "Feb 19, 2025"
|
| 34 |
-
tags:
|
| 35 |
-
- research
|
| 36 |
-
- template
|
| 37 |
-
ogImage: "https://example.com/your-og-image.png"
|
| 38 |
-
---
|
| 39 |
-
|
| 40 |
-
{/* IMPORTS */}
|
| 41 |
-
import { Image } from 'astro:assets';
|
| 42 |
-
import placeholder from '../assets/images/placeholder.jpg';
|
| 43 |
-
|
| 44 |
-
{/* CONTENT */}
|
| 45 |
-
# Hello, world
|
| 46 |
-
|
| 47 |
-
This is a short paragraph written in Markdown. Below is an example image:
|
| 48 |
-
|
| 49 |
-
<Image src={placeholder} alt="Example image" />
|
| 50 |
-
```
|
| 51 |
-
|
| 52 |
-
### Chapters
|
| 53 |
|
| 54 |
-
|
| 55 |
-
**If** your article becomes **too long**, you can **organize** it into **separate chapters**.
|
| 56 |
-
|
| 57 |
-
Simply **create a new file** in the `app/src/content/chapters` **directory**.
|
| 58 |
-
Then, **include** your new chapter in the main article by adding the following lines:
|
| 59 |
-
|
| 60 |
-
```mdx
|
| 61 |
-
import MyChapter from './chapters/my-chapter.mdx';
|
| 62 |
-
<MyChapter />
|
| 63 |
-
```
|
| 64 |
-
|
| 65 |
-
You can see an example of this in the <a href="">`app/src/content/chapters/best-pratices.mdx`</a> file.
|
| 66 |
-
|
| 67 |
-
### Theme
|
| 68 |
-
|
| 69 |
-
All **interactive elements** (buttons, inputs, cards, etc.) are themed with the **primary color** you choose. Feel free to update this color to match your **brand**.
|
| 70 |
-
|
| 71 |
-
You can **override** the theme by changing the main variable in the `app/src/styles/_variables.css` file.
|
| 72 |
-
|
| 73 |
-
You can use the **color picker** below to choose the right color.
|
| 74 |
-
<Aside>
|
| 75 |
-
<div className="">
|
| 76 |
-
<HtmlFragment src="color-picker.html" />
|
| 77 |
-
</div>
|
| 78 |
-
<Fragment slot="aside">
|
| 79 |
-
There is also a color <a href="#use-the-right-color">palette generator</a> that will help you choose the right color for your data visualizations.
|
| 80 |
-
</Fragment>
|
| 81 |
-
</Aside>
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
### Available blocks
|
| 85 |
|
| 86 |
All the following blocks are available in the article.mdx file. You can also create your own blocks by creating a new component in the components folder.
|
| 87 |
|
|
@@ -90,9 +15,10 @@ All the following blocks are available in the article.mdx file. You can also cre
|
|
| 90 |
<a className="button" href="#math">Math</a>
|
| 91 |
<a className="button" href="#images">Images</a>
|
| 92 |
<a className="button" href="#code-blocks">Code</a>
|
|
|
|
| 93 |
<a className="button" href="#citations-and-notes">Citations & notes</a>
|
| 94 |
-
<a className="button" href="#
|
| 95 |
-
<a className="button" href="#minimal-table">
|
| 96 |
<a className="button" href="#audio">Audio</a>
|
| 97 |
<a className="button" href="#embeds">Embeds</a>
|
| 98 |
</div>
|
|
@@ -344,9 +270,6 @@ Plotly version
|
|
| 344 |
<div className="plot-card">
|
| 345 |
<HtmlFragment src="line.html" />
|
| 346 |
</div>
|
| 347 |
-
<div className="plot-card">
|
| 348 |
-
<HtmlFragment src="bar.html" />
|
| 349 |
-
</div>
|
| 350 |
|
| 351 |
<small className="muted">Example</small>
|
| 352 |
```mdx
|
|
|
|
|
|
|
| 1 |
import { Image } from 'astro:assets';
|
| 2 |
import placeholder from '../../assets/images/placeholder.png';
|
| 3 |
+
import audioDemo from '../../assets/audio/audio-example.wav';
|
| 4 |
+
import HtmlFragment from '../../components/HtmlFragment.astro';
|
| 5 |
import Aside from '../../components/Aside.astro';
|
| 6 |
import Wide from '../../components/Wide.astro';
|
| 7 |
import FullBleed from '../../components/FullBleed.astro';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
## Available blocks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
All the following blocks are available in the article.mdx file. You can also create your own blocks by creating a new component in the components folder.
|
| 12 |
|
|
|
|
| 15 |
<a className="button" href="#math">Math</a>
|
| 16 |
<a className="button" href="#images">Images</a>
|
| 17 |
<a className="button" href="#code-blocks">Code</a>
|
| 18 |
+
<a className="button" href="#mermaid-diagrams">Mermaid</a>
|
| 19 |
<a className="button" href="#citations-and-notes">Citations & notes</a>
|
| 20 |
+
<a className="button" href="#placement">Placement</a>
|
| 21 |
+
<a className="button" href="#minimal-table">Minimal table</a>
|
| 22 |
<a className="button" href="#audio">Audio</a>
|
| 23 |
<a className="button" href="#embeds">Embeds</a>
|
| 24 |
</div>
|
|
|
|
| 270 |
<div className="plot-card">
|
| 271 |
<HtmlFragment src="line.html" />
|
| 272 |
</div>
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
<small className="muted">Example</small>
|
| 275 |
```mdx
|
app/src/content/chapters/best-pratices.mdx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
|
| 2 |
import { Image } from 'astro:assets';
|
| 3 |
import visualPoster from '../../assets/images/visual-vocabulary-poster.png';
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
## Best Practices
|
|
@@ -15,6 +16,26 @@ Favor **concise captions** and callouts that clarify what to look at and why it
|
|
| 15 |
**Introduce symbols and variables** the first time they appear, and prefer **well‑known identities** over custom shorthand. When formulas carry the message, add one sentence of **plain‑language interpretation** right after.
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
{/* ### Use the right color
|
| 19 |
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**.
|
| 20 |
|
|
@@ -35,12 +56,11 @@ A palette encodes **meaning** (categories, magnitudes, oppositions), preserves *
|
|
| 35 |
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**.
|
| 36 |
|
| 37 |
<figure>
|
| 38 |
-
<a href={
|
| 39 |
<Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
|
| 40 |
</a>
|
| 41 |
<figcaption>
|
| 42 |
-
A handy reference to select chart types by purpose
|
| 43 |
-
|
| 44 |
-
</figcaption>
|
| 45 |
</figure>
|
| 46 |
|
|
|
|
| 1 |
|
| 2 |
import { Image } from 'astro:assets';
|
| 3 |
import visualPoster from '../../assets/images/visual-vocabulary-poster.png';
|
| 4 |
+
import Note from '../../components/Note.astro';
|
| 5 |
|
| 6 |
|
| 7 |
## Best Practices
|
|
|
|
| 16 |
**Introduce symbols and variables** the first time they appear, and prefer **well‑known identities** over custom shorthand. When formulas carry the message, add one sentence of **plain‑language interpretation** right after.
|
| 17 |
|
| 18 |
|
| 19 |
+
<div className="note-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; align-items: start;">
|
| 20 |
+
<Note>
|
| 21 |
+
For example, in linear regression with features $x \in \mathbb{R}^d$, weights $w \in \mathbb{R}^d$, and bias $b$, the prediction is:
|
| 22 |
+
|
| 23 |
+
$$
|
| 24 |
+
\hat{y} = w^\top x + b
|
| 25 |
+
$$
|
| 26 |
+
|
| 27 |
+
A common training objective is the mean squared error over $N$ samples:
|
| 28 |
+
|
| 29 |
+
$$
|
| 30 |
+
\mathcal{L}(w,b) = \frac{1}{N} \sum_{i=1}^{N} (w^\top x_i + b - y_i)^2
|
| 31 |
+
$$
|
| 32 |
+
|
| 33 |
+
Interpretation: the model fits a hyperplane that minimizes the average squared prediction error.
|
| 34 |
+
</Note>
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
{/* ### Use the right color
|
| 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 |
|
|
|
|
| 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 |
<figure>
|
| 59 |
+
<a href={"https://raw.githubusercontent.com/Financial-Times/chart-doctor/refs/heads/main/visual-vocabulary/poster.png"} target="_blank" rel="noopener noreferrer">
|
| 60 |
<Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
|
| 61 |
</a>
|
| 62 |
<figcaption>
|
| 63 |
+
A handy reference to select chart types by purpose — click to enlarge.
|
| 64 |
+
</figcaption>
|
|
|
|
| 65 |
</figure>
|
| 66 |
|
app/src/content/chapters/getting-started.mdx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Aside from '../../components/Aside.astro';
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
### Installation
|
| 6 |
+
|
| 7 |
+
<Aside>
|
| 8 |
+
```bash
|
| 9 |
+
git lfs install
|
| 10 |
+
git lfs pull
|
| 11 |
+
cd app
|
| 12 |
+
npm install
|
| 13 |
+
```
|
| 14 |
+
<Fragment slot="aside">
|
| 15 |
+
You can also use yarn.
|
| 16 |
+
</Fragment>
|
| 17 |
+
</Aside>
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
### Development
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
npm run dev
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
### Build
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
npm run build
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Serving the `dist/` directory on any static host is enough to deliver the site.
|
| 33 |
+
|
| 34 |
+
A [slug-title].pdf and thumb.jpg are also generated at build time. You can find them in the public folder.
|
| 35 |
+
|
| 36 |
+
### Deploy
|
| 37 |
+
|
| 38 |
+
The recommended way is to **clone this [🤗 Hugging Face Space](https://huggingface.co/spaces/tfrere/science-blog-template)**.
|
| 39 |
+
|
| 40 |
+
Once you have cloned the Space, **just push your changes** to your repository.
|
| 41 |
+
|
| 42 |
+
**Every push automatically triggers a build and deploy** — no extra configuration needed.
|
app/src/content/chapters/writing-your-content.mdx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{/* IMPORTS */}
|
| 2 |
+
import { Image } from 'astro:assets';
|
| 3 |
+
import placeholder from '../../assets/images/placeholder.png';
|
| 4 |
+
import Aside from '../../components/Aside.astro';
|
| 5 |
+
import Wide from '../../components/Wide.astro';
|
| 6 |
+
import FullBleed from '../../components/FullBleed.astro';
|
| 7 |
+
import HtmlFragment from '../../components/HtmlFragment.astro';
|
| 8 |
+
import audioDemo from '../../assets/audio/audio-example.wav';
|
| 9 |
+
|
| 10 |
+
## Writing Your Content
|
| 11 |
+
|
| 12 |
+
### Introduction
|
| 13 |
+
|
| 14 |
+
Your article lives in two places
|
| 15 |
+
|
| 16 |
+
- `app/src/content/` — where you can find the `article.mdx`, `bibliography.bib` and html fragments.
|
| 17 |
+
- `app/src/assets/` — images, audio, and other static assets. (handled by git lfs)
|
| 18 |
+
|
| 19 |
+
The `article.mdx` file is a markdown file with html and astro components. It is the main file that contains your article.
|
| 20 |
+
|
| 21 |
+
<small className="muted">Example</small>
|
| 22 |
+
```mdx
|
| 23 |
+
{/* HEADER */}
|
| 24 |
+
---
|
| 25 |
+
title: "This is the main title"
|
| 26 |
+
subtitle: "This will be displayed just below the banner"
|
| 27 |
+
description: "A modern, MDX-first research article template with math, citations, and interactive figures."
|
| 28 |
+
authors:
|
| 29 |
+
- "John Doe"
|
| 30 |
+
- "Alice Martin"
|
| 31 |
+
- "Robert Brown"
|
| 32 |
+
affiliation: "Hugging Face"
|
| 33 |
+
published: "Feb 19, 2025"
|
| 34 |
+
tags:
|
| 35 |
+
- research
|
| 36 |
+
- template
|
| 37 |
+
ogImage: "https://example.com/your-og-image.png"
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
{/* IMPORTS */}
|
| 41 |
+
import { Image } from 'astro:assets';
|
| 42 |
+
import placeholder from '../assets/images/placeholder.jpg';
|
| 43 |
+
|
| 44 |
+
{/* CONTENT */}
|
| 45 |
+
# Hello, world
|
| 46 |
+
|
| 47 |
+
This is a short paragraph written in Markdown. Below is an example image:
|
| 48 |
+
|
| 49 |
+
<Image src={placeholder} alt="Example image" />
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### Markdown
|
| 53 |
+
|
| 54 |
+
You can use **markdown** to write your content. Here is the complete [markdown documentation](https://www.markdownguide.org/basic-syntax/).
|
| 55 |
+
|
| 56 |
+
<small className="muted">Example</small>
|
| 57 |
+
```mdx
|
| 58 |
+
# This is the main title
|
| 59 |
+
|
| 60 |
+
This is a short paragraph containing **bold** and *italic* text.
|
| 61 |
+
|
| 62 |
+
## This is a second level heading
|
| 63 |
+
|
| 64 |
+
- This is a list item
|
| 65 |
+
- This is a second list item
|
| 66 |
+
- This is a third list item
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
### Chapters
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
**If** your article becomes **too long** for one file, you can **organize** it into **separate chapters**.
|
| 74 |
+
|
| 75 |
+
Simply **create a new file** in the `app/src/content/chapters` **directory**.
|
| 76 |
+
Then, **include** your new chapter in the main article.
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
<small className="muted">Example</small>
|
| 80 |
+
```mdx
|
| 81 |
+
import MyChapter from './chapters/my-chapter.mdx';
|
| 82 |
+
|
| 83 |
+
<MyChapter />
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
You can see an example of this in the <a target="_blank" href="https://huggingface.co/spaces/tfrere/research-article-template/blob/main/app/src/content/chapters/best-pratices.mdx">app/src/content/chapters/best-pratices.mdx</a> file.
|
| 87 |
+
|
| 88 |
+
### Table of contents
|
| 89 |
+
|
| 90 |
+
The **Table of contents** is generated **automatically** from your **H2–H4** headings. Keep headings **short** and **descriptive**; links work on **desktop** and **mobile**.
|
| 91 |
+
|
| 92 |
+
### Theme
|
| 93 |
+
|
| 94 |
+
All **interactive elements** (buttons, inputs, cards, etc.) are themed with the **primary color** you choose.
|
| 95 |
+
|
| 96 |
+
You can **update this main color** to match your brand by changing the `--primary-color` variable in the `app/src/styles/_variables.css` file.
|
| 97 |
+
|
| 98 |
+
Use the **color picker** below to see how the primary color affects the theme.
|
| 99 |
+
|
| 100 |
+
#### Brand color
|
| 101 |
+
|
| 102 |
+
<Aside>
|
| 103 |
+
<HtmlFragment src="color-picker.html" />
|
| 104 |
+
<Fragment slot="aside">
|
| 105 |
+
You can use the color picker to select the right color.
|
| 106 |
+
|
| 107 |
+
Here is an example of an <a href="#">interactive element</a>.
|
| 108 |
+
</Fragment>
|
| 109 |
+
</Aside>
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
#### Color palettes
|
| 113 |
+
|
| 114 |
+
Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
|
| 115 |
+
|
| 116 |
+
<HtmlFragment src="palettes.html" />
|
app/src/content/fragments/banner.html
CHANGED
|
@@ -106,8 +106,8 @@
|
|
| 106 |
const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
|
| 107 |
|
| 108 |
|
| 109 |
-
// Group
|
| 110 |
-
const g = svg.selectAll('g.points').data([0]).join('g').attr('class', 'points')
|
| 111 |
|
| 112 |
// Ensure container can host an absolute tooltip
|
| 113 |
container.style.position = container.style.position || 'relative';
|
|
|
|
| 106 |
const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
|
| 107 |
|
| 108 |
|
| 109 |
+
// Group for points (no blend mode for better print/PDF visibility)
|
| 110 |
+
const g = svg.selectAll('g.points').data([0]).join('g').attr('class', 'points');
|
| 111 |
|
| 112 |
// Ensure container can host an absolute tooltip
|
| 113 |
container.style.position = container.style.position || 'relative';
|
app/src/content/fragments/color-picker.html
CHANGED
|
@@ -1,33 +1,36 @@
|
|
| 1 |
<div class="color-picker" style="width:100%; margin: 10px 0;">
|
| 2 |
<style>
|
| 3 |
.color-picker .picker__stack { display:flex; flex-direction:column; gap:12px; }
|
| 4 |
-
.color-picker .current-card { display:
|
|
|
|
|
|
|
| 5 |
.color-picker .current-main { display:flex; align-items:center; gap:12px; min-width: 0; }
|
| 6 |
.color-picker .current-swatch { width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--border-color); }
|
| 7 |
.color-picker .current-text { display:flex; flex-direction: column; line-height: 1.2; min-width: 0; }
|
| 8 |
.color-picker .current-name { font-size: 14px; font-weight: 800; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
|
| 9 |
.color-picker .current-hex, .color-picker .current-extra { font-size: 11px; color: var(--muted-color); letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
|
| 10 |
/* theme preview styles removed */
|
| 11 |
-
.color-picker .
|
| 12 |
-
.color-picker .
|
| 13 |
-
.color-picker .hue-slider { position:relative; height:16px; border-radius:10px; border:1px solid var(--border-color); background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); cursor: ew-resize; touch-action: none; flex: 1 1 320px; min-width: 200px; }
|
| 14 |
.color-picker .hue-knob { position:absolute; top:50%; left:93.6%; width:14px; height:14px; border-radius:50%; border:2px solid #fff; transform:translate(-50%, -50%); background: var(--surface-bg); z-index: 2; box-shadow: 0 0 0 1px rgba(0,0,0,.05); }
|
| 15 |
-
.color-picker .hue-slider:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
| 16 |
-
.color-picker .hue-value { font-variant-numeric: tabular-nums; color: var(--muted-color);
|
| 17 |
-
@media (max-width:
|
| 18 |
</style>
|
| 19 |
<div class="picker__stack">
|
| 20 |
<div class="current-card">
|
| 21 |
-
<div class="current-
|
| 22 |
-
<div class="current-
|
| 23 |
-
|
| 24 |
-
<div class="current-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
</div>
|
| 29 |
</div>
|
| 30 |
-
<div class="
|
| 31 |
<div class="picker__label">Hue</div>
|
| 32 |
<div class="hue-slider" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" aria-valuenow="337" tabindex="0">
|
| 33 |
<div class="hue-knob"></div>
|
|
@@ -57,7 +60,7 @@
|
|
| 57 |
};
|
| 58 |
|
| 59 |
// Minimal embedded color-name list (same as palettes)
|
| 60 |
-
const COLOR_NAMES = [{"name":"Candy Apple Red","hex":"#ff0800"},{"name":"Boiling Magma","hex":"#ff3300"},{"name":"Aerospace Orange","hex":"#ff4f00"},{"name":"Burtuqali Orange","hex":"#ff6700"},{"name":"American Orange","hex":"#ff8b00"},{"name":"Cheese","hex":"#ffa600"},{"name":"Amber","hex":"#ffbf00"},{"name":"Demonic Yellow","hex":"#ffe700"},{"name":"Bat-Signal","hex":"#feff00"},{"name":"Bitter Lime","hex":"#cfff00"},{"name":"Electric Lime","hex":"#ccff00"},{"name":"Bright Yellow Green","hex":"#9dff00"},{"name":"Lasting Lime","hex":"#88ff00"},{"name":"Bright Green","hex":"#66ff00"},{"name":"Chlorophyll Green","hex":"#4aff00"},{"name":"Green Screen","hex":"#22ff00"},{"name":"Electric Pickle","hex":"#00ff04"},{"name":"Acid","hex":"#00ff22"},{"name":"Lucent Lime","hex":"#00ff33"},{"name":"Cathode Green","hex":"#00ff55"},{"name":"Booger Buster","hex":"#00ff77"},{"name":"Green Gas","hex":"#00ff99"},{"name":"Enthusiasm","hex":"#00ffaa"},{"name":"Ice Ice Baby","hex":"#00ffdd"},{"name":"Master Sword Blue","hex":"#00ffee"},{"name":"Agressive Aqua","hex":"#00fbff"},{"name":"Vivid Sky Blue","hex":"#00ccff"},{"name":"Capri","hex":"#00bfff"},{"name":"Sky of Magritte","hex":"#0099ff"},{"name":"Azure","hex":"#007fff"},{"name":"Blue Ribbon","hex":"#0066ff"},{"name":"Blinking Blue","hex":"#0033ff"},{"name":"Icelandic Water","hex":"#0011ff"},{"name":"Blue","hex":"#0000ff"},{"name":"Blue Pencil","hex":"#2200ff"},{"name":"Electric Ultramarine","hex":"#3f00ff"},{"name":"Aladdin's Feather","hex":"#5500ff"},{"name":"Purple Climax","hex":"#8800ff"},{"name":"Amethyst Ganzstar","hex":"#8f00ff"},{"name":"Electric Purple","hex":"#bf00ff"},{"name":"Phlox","hex":"#df00ff"},{"name":"Brusque Pink","hex":"#ee00ff"},{"name":"Bright Magenta","hex":"#ff08e8"},{"name":"
|
| 61 |
if (!window.__colorNames) window.__colorNames = COLOR_NAMES;
|
| 62 |
|
| 63 |
// Shared event bus so multiple instances stay in sync
|
|
@@ -95,6 +98,10 @@
|
|
| 95 |
const bus = window.__colorPickerBus;
|
| 96 |
const instanceId = Math.random().toString(36).slice(2);
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
const getName = (hex) => {
|
| 99 |
const list = (window.__colorNames && window.__colorNames.length) ? window.__colorNames : COLOR_NAMES;
|
| 100 |
if (list && window.chroma) {
|
|
@@ -113,8 +120,11 @@
|
|
| 113 |
};
|
| 114 |
|
| 115 |
const updateUI = (h, adjusting) => {
|
| 116 |
-
const
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
if (hueValue) hueValue.textContent = `${Math.round(h)}°`;
|
| 119 |
if (slider) slider.setAttribute('aria-valuenow', String(Math.round(h)));
|
| 120 |
// Use LCH for consistent chroma across hues
|
|
@@ -140,15 +150,18 @@
|
|
| 140 |
const hoverL = Math.max(0, Math.min(100, L - 8));
|
| 141 |
const hoverHex = chroma.lch(hoverL, C, h).hex();
|
| 142 |
const rootEl = document.documentElement;
|
| 143 |
-
rootEl.style.setProperty('--primary', baseHex);
|
| 144 |
-
rootEl.style.setProperty('--primary-hover', hoverHex);
|
| 145 |
};
|
| 146 |
|
| 147 |
const getHueFromEvent = (ev) => {
|
| 148 |
const rect = slider.getBoundingClientRect();
|
| 149 |
const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
|
| 150 |
const x = clientX - rect.left;
|
| 151 |
-
const
|
|
|
|
|
|
|
|
|
|
| 152 |
return t * 360;
|
| 153 |
};
|
| 154 |
|
|
@@ -160,7 +173,7 @@
|
|
| 160 |
|
| 161 |
// Init depuis la couleur de thème si disponible
|
| 162 |
try {
|
| 163 |
-
const cssPrimary = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim();
|
| 164 |
if (cssPrimary) {
|
| 165 |
const initH = chroma(cssPrimary).get('hsl.h') || 0;
|
| 166 |
updateUI(initH, false);
|
|
|
|
| 1 |
<div class="color-picker" style="width:100%; margin: 10px 0;">
|
| 2 |
<style>
|
| 3 |
.color-picker .picker__stack { display:flex; flex-direction:column; gap:12px; }
|
| 4 |
+
.color-picker .current-card { display:grid; grid-template-columns: 30% 70%; align-items: center; gap:14px; padding:14px 32px 14px 16px; border:1px solid var(--border-color); background: var(--surface-bg); border-radius: 12px; }
|
| 5 |
+
.color-picker .current-left { display:flex; flex-direction: column; gap:8px; min-width: 0; }
|
| 6 |
+
.color-picker .current-right { display:flex; flex-direction: column; gap:8px; padding-left: 14px; border-left: 1px solid var(--border-color); }
|
| 7 |
.color-picker .current-main { display:flex; align-items:center; gap:12px; min-width: 0; }
|
| 8 |
.color-picker .current-swatch { width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--border-color); }
|
| 9 |
.color-picker .current-text { display:flex; flex-direction: column; line-height: 1.2; min-width: 0; }
|
| 10 |
.color-picker .current-name { font-size: 14px; font-weight: 800; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
|
| 11 |
.color-picker .current-hex, .color-picker .current-extra { font-size: 11px; color: var(--muted-color); letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
|
| 12 |
/* theme preview styles removed */
|
| 13 |
+
.color-picker .picker__label { font-weight:700; font-size: 12px; color: var(--muted-color); text-transform: uppercase; letter-spacing: .02em; }
|
| 14 |
+
.color-picker .hue-slider { position:relative; height:16px; border-radius:10px; border:1px solid var(--border-color); background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); cursor: ew-resize; touch-action: none; flex: 1 1 auto; min-width: 200px; }
|
|
|
|
| 15 |
.color-picker .hue-knob { position:absolute; top:50%; left:93.6%; width:14px; height:14px; border-radius:50%; border:2px solid #fff; transform:translate(-50%, -50%); background: var(--surface-bg); z-index: 2; box-shadow: 0 0 0 1px rgba(0,0,0,.05); }
|
| 16 |
+
.color-picker .hue-slider:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
| 17 |
+
.color-picker .hue-value { font-variant-numeric: tabular-nums; color: var(--muted-color); font-size: 12px; }
|
| 18 |
+
@media (max-width: 720px) { .color-picker .current-card { grid-template-columns: 1fr; } .color-picker .current-right { padding-left: 0; border-left: none; } }
|
| 19 |
</style>
|
| 20 |
<div class="picker__stack">
|
| 21 |
<div class="current-card">
|
| 22 |
+
<div class="current-left">
|
| 23 |
+
<div class="current-main">
|
| 24 |
+
<div class="current-swatch" aria-label="Current color" title="Current color"></div>
|
| 25 |
+
<div class="current-text">
|
| 26 |
+
<div class="current-name">—</div>
|
| 27 |
+
<div class="current-hex">—</div>
|
| 28 |
+
<div class="current-extra current-lch">—</div>
|
| 29 |
+
<div class="current-extra current-rgb">—</div>
|
| 30 |
+
</div>
|
| 31 |
</div>
|
| 32 |
</div>
|
| 33 |
+
<div class="current-right">
|
| 34 |
<div class="picker__label">Hue</div>
|
| 35 |
<div class="hue-slider" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" aria-valuenow="337" tabindex="0">
|
| 36 |
<div class="hue-knob"></div>
|
|
|
|
| 60 |
};
|
| 61 |
|
| 62 |
// Minimal embedded color-name list (same as palettes)
|
| 63 |
+
const COLOR_NAMES = [{"name":"Candy Apple Red","hex":"#ff0800"},{"name":"Boiling Magma","hex":"#ff3300"},{"name":"Aerospace Orange","hex":"#ff4f00"},{"name":"Burtuqali Orange","hex":"#ff6700"},{"name":"American Orange","hex":"#ff8b00"},{"name":"Cheese","hex":"#ffa600"},{"name":"Amber","hex":"#ffbf00"},{"name":"Demonic Yellow","hex":"#ffe700"},{"name":"Bat-Signal","hex":"#feff00"},{"name":"Bitter Lime","hex":"#cfff00"},{"name":"Electric Lime","hex":"#ccff00"},{"name":"Bright Yellow Green","hex":"#9dff00"},{"name":"Lasting Lime","hex":"#88ff00"},{"name":"Bright Green","hex":"#66ff00"},{"name":"Chlorophyll Green","hex":"#4aff00"},{"name":"Green Screen","hex":"#22ff00"},{"name":"Electric Pickle","hex":"#00ff04"},{"name":"Acid","hex":"#00ff22"},{"name":"Lucent Lime","hex":"#00ff33"},{"name":"Cathode Green","hex":"#00ff55"},{"name":"Booger Buster","hex":"#00ff77"},{"name":"Green Gas","hex":"#00ff99"},{"name":"Enthusiasm","hex":"#00ffaa"},{"name":"Ice Ice Baby","hex":"#00ffdd"},{"name":"Master Sword Blue","hex":"#00ffee"},{"name":"Agressive Aqua","hex":"#00fbff"},{"name":"Vivid Sky Blue","hex":"#00ccff"},{"name":"Capri","hex":"#00bfff"},{"name":"Sky of Magritte","hex":"#0099ff"},{"name":"Azure","hex":"#007fff"},{"name":"Blue Ribbon","hex":"#0066ff"},{"name":"Blinking Blue","hex":"#0033ff"},{"name":"Icelandic Water","hex":"#0011ff"},{"name":"Blue","hex":"#0000ff"},{"name":"Blue Pencil","hex":"#2200ff"},{"name":"Electric Ultramarine","hex":"#3f00ff"},{"name":"Aladdin's Feather","hex":"#5500ff"},{"name":"Purple Climax","hex":"#8800ff"},{"name":"Amethyst Ganzstar","hex":"#8f00ff"},{"name":"Electric Purple","hex":"#bf00ff"},{"name":"Phlox","hex":"#df00ff"},{"name":"Brusque Pink","hex":"#ee00ff"},{"name":"Bright Magenta","hex":"#ff08e8"},{"name":"Big bang Pink","hex":"#ff00bb"},{"name":"Mean Girls Lipstick","hex":"#ff00ae"},{"name":"Pink","hex":"#ff0099"},{"name":"Hot Flamingoes","hex":"#ff005d"},{"name":"Blazing Dragonfruit","hex":"#ff0054"},{"name":"Carmine Red","hex":"#ff0038"},{"name":"Bright Red","hex":"#ff000d"}];
|
| 64 |
if (!window.__colorNames) window.__colorNames = COLOR_NAMES;
|
| 65 |
|
| 66 |
// Shared event bus so multiple instances stay in sync
|
|
|
|
| 98 |
const bus = window.__colorPickerBus;
|
| 99 |
const instanceId = Math.random().toString(36).slice(2);
|
| 100 |
|
| 101 |
+
const getKnobRadius = () => {
|
| 102 |
+
try { const w = knob ? knob.getBoundingClientRect().width : 0; return w ? w / 2 : 8; } catch { return 8; }
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
const getName = (hex) => {
|
| 106 |
const list = (window.__colorNames && window.__colorNames.length) ? window.__colorNames : COLOR_NAMES;
|
| 107 |
if (list && window.chroma) {
|
|
|
|
| 120 |
};
|
| 121 |
|
| 122 |
const updateUI = (h, adjusting) => {
|
| 123 |
+
const rect = slider.getBoundingClientRect();
|
| 124 |
+
const r = Math.min(getKnobRadius(), Math.max(0, rect.width / 2 - 1));
|
| 125 |
+
const t = Math.max(0, Math.min(1, (h / 360)));
|
| 126 |
+
const leftPx = r + t * Math.max(0, (rect.width - 2 * r));
|
| 127 |
+
if (knob) knob.style.left = (leftPx / rect.width * 100) + '%';
|
| 128 |
if (hueValue) hueValue.textContent = `${Math.round(h)}°`;
|
| 129 |
if (slider) slider.setAttribute('aria-valuenow', String(Math.round(h)));
|
| 130 |
// Use LCH for consistent chroma across hues
|
|
|
|
| 150 |
const hoverL = Math.max(0, Math.min(100, L - 8));
|
| 151 |
const hoverHex = chroma.lch(hoverL, C, h).hex();
|
| 152 |
const rootEl = document.documentElement;
|
| 153 |
+
rootEl.style.setProperty('--primary-color', baseHex);
|
| 154 |
+
rootEl.style.setProperty('--primary-color-hover', hoverHex);
|
| 155 |
};
|
| 156 |
|
| 157 |
const getHueFromEvent = (ev) => {
|
| 158 |
const rect = slider.getBoundingClientRect();
|
| 159 |
const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
|
| 160 |
const x = clientX - rect.left;
|
| 161 |
+
const r = Math.min(getKnobRadius(), Math.max(0, rect.width / 2 - 1));
|
| 162 |
+
const effX = Math.max(r, Math.min(rect.width - r, x));
|
| 163 |
+
const denom = Math.max(1, rect.width - 2 * r);
|
| 164 |
+
const t = (effX - r) / denom;
|
| 165 |
return t * 360;
|
| 166 |
};
|
| 167 |
|
|
|
|
| 173 |
|
| 174 |
// Init depuis la couleur de thème si disponible
|
| 175 |
try {
|
| 176 |
+
const cssPrimary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim();
|
| 177 |
if (cssPrimary) {
|
| 178 |
const initH = chroma(cssPrimary).get('hsl.h') || 0;
|
| 179 |
updateUI(initH, false);
|
app/src/content/fragments/d3-bar.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
| 4 |
.d3-bar .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
|
| 5 |
.d3-bar .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
|
| 6 |
[data-theme="dark"] .d3-bar .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
|
| 7 |
-
.d3-bar .controls select:hover { border-color: var(--primary); }
|
| 8 |
-
.d3-bar .controls select:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 9 |
.d3-bar .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
|
| 10 |
</style>
|
| 11 |
<script>
|
|
@@ -31,7 +31,7 @@
|
|
| 31 |
const components = [
|
| 32 |
{ key: 'parameters', color: 'rgb(78, 165, 183)' },
|
| 33 |
{ key: 'gradients', color: 'rgb(227, 138, 66)' },
|
| 34 |
-
{ key: 'optimizer', color: 'var(--primary)' },
|
| 35 |
{ key: 'activations', color: 'rgb(206, 192, 250)' },
|
| 36 |
];
|
| 37 |
const modelSizes = ["1B","3B","8B","70B","405B"];
|
|
@@ -84,7 +84,8 @@
|
|
| 84 |
|
| 85 |
// State
|
| 86 |
let currentSize = modelSizes[0];
|
| 87 |
-
let currentMode = '
|
|
|
|
| 88 |
|
| 89 |
// Layout & scales
|
| 90 |
let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
|
|
@@ -130,7 +131,7 @@
|
|
| 130 |
// Axes
|
| 131 |
gAxes.selectAll('*').remove();
|
| 132 |
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 133 |
-
gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('
|
| 134 |
|
| 135 |
// Axis labels
|
| 136 |
gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Sequence Length');
|
|
|
|
| 4 |
.d3-bar .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
|
| 5 |
.d3-bar .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
|
| 6 |
[data-theme="dark"] .d3-bar .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
|
| 7 |
+
.d3-bar .controls select:hover { border-color: var(--primary-color); }
|
| 8 |
+
.d3-bar .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 9 |
.d3-bar .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
|
| 10 |
</style>
|
| 11 |
<script>
|
|
|
|
| 31 |
const components = [
|
| 32 |
{ key: 'parameters', color: 'rgb(78, 165, 183)' },
|
| 33 |
{ key: 'gradients', color: 'rgb(227, 138, 66)' },
|
| 34 |
+
{ key: 'optimizer', color: 'var(--primary-color)' },
|
| 35 |
{ key: 'activations', color: 'rgb(206, 192, 250)' },
|
| 36 |
];
|
| 37 |
const modelSizes = ["1B","3B","8B","70B","405B"];
|
|
|
|
| 84 |
|
| 85 |
// State
|
| 86 |
let currentSize = modelSizes[0];
|
| 87 |
+
let currentMode = 'selective';
|
| 88 |
+
selRecomp.value = currentMode;
|
| 89 |
|
| 90 |
// Layout & scales
|
| 91 |
let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
|
|
|
|
| 131 |
// Axes
|
| 132 |
gAxes.selectAll('*').remove();
|
| 133 |
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 134 |
+
gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('~f'))).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 135 |
|
| 136 |
// Axis labels
|
| 137 |
gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Sequence Length');
|
app/src/content/fragments/d3-line.html
CHANGED
|
@@ -21,16 +21,16 @@
|
|
| 21 |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 22 |
}
|
| 23 |
.d3-line .d3-line__controls select:hover {
|
| 24 |
-
border-color: var(--primary);
|
| 25 |
}
|
| 26 |
.d3-line .d3-line__controls select:focus {
|
| 27 |
-
border-color: var(--primary);
|
| 28 |
box-shadow: 0 0 0 3px rgba(232,137,171,.25);
|
| 29 |
outline: none;
|
| 30 |
}
|
| 31 |
.d3-line .d3-line__controls label { gap: 8px; }
|
| 32 |
|
| 33 |
-
/* Range slider themed with --primary */
|
| 34 |
.d3-line .d3-line__controls input[type="range"] {
|
| 35 |
-webkit-appearance: none;
|
| 36 |
appearance: none;
|
|
@@ -51,7 +51,7 @@
|
|
| 51 |
width: 16px;
|
| 52 |
height: 16px;
|
| 53 |
border-radius: 50%;
|
| 54 |
-
background: var(--primary);
|
| 55 |
border: 2px solid var(--on-primary);
|
| 56 |
margin-top: -5px;
|
| 57 |
cursor: pointer;
|
|
@@ -65,12 +65,12 @@
|
|
| 65 |
width: 16px;
|
| 66 |
height: 16px;
|
| 67 |
border-radius: 50%;
|
| 68 |
-
background: var(--primary);
|
| 69 |
border: 2px solid var(--on-primary);
|
| 70 |
cursor: pointer;
|
| 71 |
}
|
| 72 |
/* Improved line color via CSS */
|
| 73 |
-
.d3-line .lines path.improved { stroke: var(--primary); }
|
| 74 |
</style>
|
| 75 |
<script>
|
| 76 |
(() => {
|
|
@@ -181,7 +181,7 @@
|
|
| 181 |
|
| 182 |
// Colors
|
| 183 |
const colorBase = '#64748b'; // slate-500
|
| 184 |
-
const colorImproved = 'var(--primary)';
|
| 185 |
const colorTarget = '#4b5563'; // gray-600
|
| 186 |
const legendBgLight = 'rgba(255,255,255,0.85)';
|
| 187 |
const legendBgDark = 'rgba(17,17,23,0.85)';
|
|
@@ -221,13 +221,13 @@
|
|
| 221 |
.y((d) => yScale(d));
|
| 222 |
|
| 223 |
const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
|
| 224 |
-
const pathImp = gLines.append('path').attr('class', 'improved').attr('fill', 'none').style('stroke', 'var(--primary)').attr('stroke-width', 2);
|
| 225 |
const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
|
| 226 |
|
| 227 |
// Hover elements
|
| 228 |
const hoverLine = gHover.append('line').attr('stroke-width', 1);
|
| 229 |
const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
|
| 230 |
-
const hoverDotI = gHover.append('circle').attr('class', 'improved').attr('r', 3.5).style('fill', 'var(--primary)').attr('stroke', '#fff').attr('stroke-width', 1);
|
| 231 |
const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
|
| 232 |
|
| 233 |
const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
|
|
|
|
| 21 |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 22 |
}
|
| 23 |
.d3-line .d3-line__controls select:hover {
|
| 24 |
+
border-color: var(--primary-color);
|
| 25 |
}
|
| 26 |
.d3-line .d3-line__controls select:focus {
|
| 27 |
+
border-color: var(--primary-color);
|
| 28 |
box-shadow: 0 0 0 3px rgba(232,137,171,.25);
|
| 29 |
outline: none;
|
| 30 |
}
|
| 31 |
.d3-line .d3-line__controls label { gap: 8px; }
|
| 32 |
|
| 33 |
+
/* Range slider themed with --primary-color */
|
| 34 |
.d3-line .d3-line__controls input[type="range"] {
|
| 35 |
-webkit-appearance: none;
|
| 36 |
appearance: none;
|
|
|
|
| 51 |
width: 16px;
|
| 52 |
height: 16px;
|
| 53 |
border-radius: 50%;
|
| 54 |
+
background: var(--primary-color);
|
| 55 |
border: 2px solid var(--on-primary);
|
| 56 |
margin-top: -5px;
|
| 57 |
cursor: pointer;
|
|
|
|
| 65 |
width: 16px;
|
| 66 |
height: 16px;
|
| 67 |
border-radius: 50%;
|
| 68 |
+
background: var(--primary-color);
|
| 69 |
border: 2px solid var(--on-primary);
|
| 70 |
cursor: pointer;
|
| 71 |
}
|
| 72 |
/* Improved line color via CSS */
|
| 73 |
+
.d3-line .lines path.improved { stroke: var(--primary-color); }
|
| 74 |
</style>
|
| 75 |
<script>
|
| 76 |
(() => {
|
|
|
|
| 181 |
|
| 182 |
// Colors
|
| 183 |
const colorBase = '#64748b'; // slate-500
|
| 184 |
+
const colorImproved = 'var(--primary-color)';
|
| 185 |
const colorTarget = '#4b5563'; // gray-600
|
| 186 |
const legendBgLight = 'rgba(255,255,255,0.85)';
|
| 187 |
const legendBgDark = 'rgba(17,17,23,0.85)';
|
|
|
|
| 221 |
.y((d) => yScale(d));
|
| 222 |
|
| 223 |
const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
|
| 224 |
+
const pathImp = gLines.append('path').attr('class', 'improved').attr('fill', 'none').style('stroke', 'var(--primary-color)').attr('stroke-width', 2);
|
| 225 |
const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
|
| 226 |
|
| 227 |
// Hover elements
|
| 228 |
const hoverLine = gHover.append('line').attr('stroke-width', 1);
|
| 229 |
const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
|
| 230 |
+
const hoverDotI = gHover.append('circle').attr('class', 'improved').attr('r', 3.5).style('fill', 'var(--primary-color)').attr('stroke', '#fff').attr('stroke-width', 1);
|
| 231 |
const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
|
| 232 |
|
| 233 |
const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
|
app/src/content/fragments/heatmap.html
CHANGED
|
@@ -26,7 +26,7 @@
|
|
| 26 |
{ key: 'model', label: 'Model', color: '#a78bfa' },
|
| 27 |
{ key: 'forward', label: 'Forward', color: '#14b8a6' },
|
| 28 |
{ key: 'backward', label: 'Backward', color: '#f59e0b' },
|
| 29 |
-
{ key: 'gradients', label: 'Gradients', color: 'var(--primary)' },
|
| 30 |
{ key: 'optimization', label: 'Optimization', color: '#10b981' },
|
| 31 |
{ key: 'updated', label: 'Updated', color: '#7c3aed' },
|
| 32 |
];
|
|
|
|
| 26 |
{ key: 'model', label: 'Model', color: '#a78bfa' },
|
| 27 |
{ key: 'forward', label: 'Forward', color: '#14b8a6' },
|
| 28 |
{ key: 'backward', label: 'Backward', color: '#f59e0b' },
|
| 29 |
+
{ key: 'gradients', label: 'Gradients', color: 'var(--primary-color)' },
|
| 30 |
{ key: 'optimization', label: 'Optimization', color: '#10b981' },
|
| 31 |
{ key: 'updated', label: 'Updated', color: '#7c3aed' },
|
| 32 |
];
|
app/src/content/fragments/palettes.html
CHANGED
|
@@ -1,337 +1,116 @@
|
|
| 1 |
<div class="palettes" style="width:100%; margin: 10px 0;">
|
| 2 |
<style>
|
| 3 |
-
.palettes .palettes__grid { display: grid; grid-template-columns:
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
.palettes .palette-
|
| 7 |
-
.palettes .palette-
|
| 8 |
-
.palettes .palette-
|
| 9 |
-
.palettes .palette-
|
| 10 |
-
.palettes .
|
| 11 |
-
.palettes .
|
| 12 |
-
.palettes .
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
.palettes .current-text { display: flex; flex-direction: column; line-height: 1.1; }
|
| 20 |
-
.palettes .current-name { font-size: 14px; font-weight: 700; color: var(--text-color); }
|
| 21 |
-
.palettes .current-hex { font-size: 11px; color: var(--muted-color); letter-spacing: .02em; }
|
| 22 |
-
.palettes .cvd-select { padding: 4px 8px; font-size: 12px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color); }
|
| 23 |
</style>
|
| 24 |
-
<div class="palettes__controls" style="display:flex; flex-direction:column; gap:10px; margin-bottom:24px;">
|
| 25 |
-
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
| 26 |
-
<div style="font-weight:700;">Pick a hue</div>
|
| 27 |
-
<div class="hue-value" style="font-variant-numeric: tabular-nums; color: var(--muted-color);">H 337°</div>
|
| 28 |
-
</div>
|
| 29 |
-
<div class="hue-slider" style="position:relative; height:18px; border-radius:10px; border:1px solid var(--border-color); background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); cursor: ew-resize; touch-action: none;">
|
| 30 |
-
<div class="hue-knob" style="position:absolute; top:50%; left:93.6%; width:14px; height:14px; border-radius:50%; border:2px solid #fff; transform:translate(-50%, -50%); background: var(--surface-bg); z-index: 2;"></div>
|
| 31 |
-
</div>
|
| 32 |
-
<div class="palettes__meta">
|
| 33 |
-
<div class="current-color">
|
| 34 |
-
<div class="current-swatch" aria-label="Current color" title="Current color"></div>
|
| 35 |
-
<div class="current-text">
|
| 36 |
-
<div class="current-name">—</div>
|
| 37 |
-
<div class="current-hex">—</div>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
<label style="display:flex; align-items:center; gap:8px;">
|
| 41 |
-
<span style="font-size:12px; color: var(--muted-color);">Color blindness preview</span>
|
| 42 |
-
<select class="cvd-select" title="Choose a color‑blindness simulation">
|
| 43 |
-
<option value="none">None</option>
|
| 44 |
-
<option value="protanopia">Protanopia (<2%)</option>
|
| 45 |
-
<option value="deuteranopia">Deuteranopia (<2%)</option>
|
| 46 |
-
<option value="tritanopia">Tritanopia (<0.1%)</option>
|
| 47 |
-
<option value="achromatopsia">Monochromacy (<0.1%)</option>
|
| 48 |
-
</select>
|
| 49 |
-
</label>
|
| 50 |
-
</div>
|
| 51 |
-
</div>
|
| 52 |
<div class="palettes__grid"></div>
|
| 53 |
</div>
|
| 54 |
<script>
|
| 55 |
(() => {
|
| 56 |
-
const ensureLibs = (cb) => {
|
| 57 |
const loadScript = (id, src, onload, onerror) => {
|
| 58 |
let s = document.getElementById(id);
|
| 59 |
if (s) { return onload && onload(); }
|
| 60 |
-
|
| 61 |
-
s.id = id;
|
| 62 |
-
s.src = src;
|
| 63 |
-
s.async = true;
|
| 64 |
if (onload) s.addEventListener('load', onload, { once: true });
|
| 65 |
if (onerror) s.addEventListener('error', onerror, { once: true });
|
| 66 |
document.head.appendChild(s);
|
| 67 |
};
|
| 68 |
-
const ensureD3 = (next) => {
|
| 69 |
-
if (window.d3) return next();
|
| 70 |
-
loadScript('d3-cdn', 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js', next, () => {
|
| 71 |
-
loadScript('d3-cdn-fallback', 'https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js', next);
|
| 72 |
-
});
|
| 73 |
-
};
|
| 74 |
const ensureChroma = (next) => {
|
| 75 |
if (window.chroma) return next();
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
});
|
| 79 |
};
|
| 80 |
-
// ntc.js removed; rely on embedded color-name list
|
| 81 |
-
ensureD3(() => ensureChroma(cb));
|
| 82 |
-
};
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
const
|
| 89 |
-
console.log('[palettes] bootstrap start');
|
| 90 |
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
|
| 91 |
const root = mount && mount.closest('.palettes') ? mount.closest('.palettes') : document.querySelector('.palettes');
|
| 92 |
-
if (!root
|
| 93 |
-
|
| 94 |
const grid = root.querySelector('.palettes__grid');
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
const
|
| 98 |
-
const
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
}
|
| 115 |
-
const bus = window.__colorPickerBus;
|
| 116 |
-
const instanceId = Math.random().toString(36).slice(2);
|
| 117 |
-
|
| 118 |
-
// Cards data with full descriptions
|
| 119 |
-
const cards = [
|
| 120 |
-
{ key: 'categorical', title: 'Categorical', desc: 'Categorical colors help users map non-numeric meaning to objects in a visualization. These are designed to be visually distinct from one another. Maximum of six.', generator: (base) => {
|
| 121 |
-
// Generate in LCH for perceptually distinct colors
|
| 122 |
-
const baseH = chroma(base).get('hsl.h');
|
| 123 |
-
const L = 70; // comfortable lightness
|
| 124 |
-
const C = 80; // moderate chroma to stay within gamut
|
| 125 |
-
return Array.from({ length: 6 }, (_, i) => chroma.lch(L, C, (baseH + i * 60) % 360).hex());
|
| 126 |
-
} },
|
| 127 |
-
{ key: 'sequential', title: 'Sequential', desc: 'Sequential colors have numeric meaning. These are a gradation of colors that go from light to dark. They are used in a heatmap context.', generator: (base) => {
|
| 128 |
-
const c = chroma(base).saturate(0.3);
|
| 129 |
-
return chroma
|
| 130 |
-
.scale([c.brighten(2), c, c.darken(2)])
|
| 131 |
-
.mode('lab')
|
| 132 |
-
.correctLightness(true)
|
| 133 |
-
.colors(6);
|
| 134 |
-
} },
|
| 135 |
-
{ key: 'diverging', title: 'Diverging', desc: 'Diverging colors also have numeric meaning. They’re useful when dealing with negative values or ranges that have two extremes with a baseline in the middle.', generator: (base) => {
|
| 136 |
-
const c = chroma(base);
|
| 137 |
-
const baseH = c.get('hsl.h');
|
| 138 |
-
const compH = (baseH + 180) % 360;
|
| 139 |
-
const left = chroma.hsl(compH, 0.75, 0.55);
|
| 140 |
-
const right = chroma.hsl(baseH, 0.75, 0.55);
|
| 141 |
-
const center = '#ffffff';
|
| 142 |
-
// Build two symmetric ramps that meet (without duplicating the neutral)
|
| 143 |
-
const leftRamp = chroma.scale([left, center]).mode('lch').correctLightness(true).colors(4);
|
| 144 |
-
const rightRamp = chroma.scale([center, right]).mode('lch').correctLightness(true).colors(4);
|
| 145 |
-
return [leftRamp[0], leftRamp[1], leftRamp[2], rightRamp[1], rightRamp[2], rightRamp[3]];
|
| 146 |
-
} }
|
| 147 |
-
];
|
| 148 |
-
|
| 149 |
-
// Render cards scaffolding
|
| 150 |
-
const cardSel = d3.select(grid).selectAll('.palette-card').data(cards, d => d.key);
|
| 151 |
-
const enter = cardSel.enter().append('div').attr('class', 'palette-card')
|
| 152 |
-
.style('border', '1px solid var(--border-color)')
|
| 153 |
-
.style('border-radius', '10px')
|
| 154 |
-
.style('background', 'var(--surface-bg)')
|
| 155 |
-
.style('padding', '16px 14px 12px')
|
| 156 |
-
.style('display', 'flex')
|
| 157 |
-
.style('flex-direction', 'column')
|
| 158 |
-
.style('gap', '10px')
|
| 159 |
-
.style('min-height', '240px');
|
| 160 |
-
|
| 161 |
-
enter.append('div').attr('class', 'palette-card__header');
|
| 162 |
-
|
| 163 |
-
enter.append('div').attr('class', 'palette-card__badge');
|
| 164 |
-
|
| 165 |
-
enter.append('div').attr('class', 'palette-card__title')
|
| 166 |
-
.style('text-align', 'center')
|
| 167 |
-
.style('font-weight', '800')
|
| 168 |
-
.style('font-size', '28px')
|
| 169 |
-
.text(d => d.title);
|
| 170 |
-
|
| 171 |
-
enter.append('div').attr('class', 'palette-card__desc')
|
| 172 |
-
.style('text-align', 'center')
|
| 173 |
-
.style('color', 'var(--muted-color)')
|
| 174 |
-
.style('line-height', '1.6')
|
| 175 |
-
.style('font-size', '15px')
|
| 176 |
-
.text(d => d.desc);
|
| 177 |
-
|
| 178 |
-
const footer = enter.append('div').attr('class', 'palette-card__footer');
|
| 179 |
-
footer.append('div').attr('class', 'palette-card__swatches');
|
| 180 |
-
footer.append('button').attr('class', 'copy-btn').text('Copy palette');
|
| 181 |
-
|
| 182 |
-
// Rendering
|
| 183 |
-
const renderPalettes = (h) => {
|
| 184 |
-
console.log('[palettes] renderPalettes', h);
|
| 185 |
-
const base = chroma.hsl(h, 0.67, 0.72);
|
| 186 |
-
// Update CSS variables for live theming only during interaction
|
| 187 |
-
try {
|
| 188 |
-
if (isAdjusting) {
|
| 189 |
-
const baseHex = base.hex();
|
| 190 |
-
const hoverHex = chroma(base).darken(0.6).hex();
|
| 191 |
-
const rootEl = document.documentElement;
|
| 192 |
-
rootEl.style.setProperty('--primary', baseHex);
|
| 193 |
-
rootEl.style.setProperty('--primary-hover', hoverHex);
|
| 194 |
-
}
|
| 195 |
-
} catch (e) {
|
| 196 |
-
console.warn('[palettes] failed updating CSS vars', e);
|
| 197 |
-
}
|
| 198 |
-
const uniformText = (bg) => (chroma(bg).luminance() > 0.45 ? '#111' : '#fff');
|
| 199 |
-
|
| 200 |
-
// Update current swatch + name
|
| 201 |
-
if (currentSwatch) currentSwatch.style.background = base.hex();
|
| 202 |
-
const getName = (hex) => {
|
| 203 |
-
// Prefer embedded reduced list
|
| 204 |
-
const list = (window.__colorNames && window.__colorNames.length) ? window.__colorNames : COLOR_NAMES;
|
| 205 |
-
if (list && window.chroma) {
|
| 206 |
-
let bestName = null; let best = Infinity;
|
| 207 |
-
for (let i = 0; i < list.length; i++) {
|
| 208 |
-
const item = list[i];
|
| 209 |
-
const d = (chroma.deltaE ? chroma.deltaE(hex, item.hex) : chroma.distance(hex, item.hex, 'lab'));
|
| 210 |
-
if (d < best) { best = d; bestName = item.name; }
|
| 211 |
-
}
|
| 212 |
-
if (bestName) return bestName;
|
| 213 |
-
}
|
| 214 |
-
// Hue-based coarse fallback
|
| 215 |
-
const hh = chroma(hex).get('hsl.h') || 0;
|
| 216 |
-
const labels = ['Red','Orange','Yellow','Lime','Green','Cyan','Blue','Indigo','Violet','Magenta'];
|
| 217 |
-
const idx = Math.round(((hh % 360) / 360) * (labels.length - 1));
|
| 218 |
-
return labels[idx];
|
| 219 |
-
};
|
| 220 |
-
const hexStr = base.hex().toUpperCase();
|
| 221 |
-
if (currentName) currentName.textContent = getName(hexStr);
|
| 222 |
-
if (currentHex) currentHex.textContent = hexStr;
|
| 223 |
-
d3.select(root).selectAll('.palette-card').each(function(d) {
|
| 224 |
-
const palette = d.generator(base);
|
| 225 |
-
console.log('[palettes] palette', d.key, palette.length, palette);
|
| 226 |
-
const sw = d3.select(this).select('.palette-card__swatches');
|
| 227 |
-
const data = palette.slice(0, 6);
|
| 228 |
-
const s = sw.selectAll('.sw').data(data, (c, i) => i);
|
| 229 |
-
const sEnter = s.enter().append('div').attr('class', 'sw');
|
| 230 |
-
sEnter.merge(s)
|
| 231 |
-
.style('background', c => c)
|
| 232 |
-
.text('');
|
| 233 |
-
s.exit().remove();
|
| 234 |
-
|
| 235 |
-
// Hook up copy button (keeps palette visible)
|
| 236 |
-
const btn = d3.select(this).select('.copy-btn');
|
| 237 |
-
btn.on('click', async () => {
|
| 238 |
-
const json = JSON.stringify(data, null, 2);
|
| 239 |
-
try {
|
| 240 |
-
await navigator.clipboard.writeText(json);
|
| 241 |
-
const old = btn.text(); btn.text('Copied!'); setTimeout(() => btn.text(old), 900);
|
| 242 |
-
} catch {
|
| 243 |
-
window.prompt('Copy palette', json);
|
| 244 |
-
}
|
| 245 |
-
});
|
| 246 |
-
|
| 247 |
-
// Update header gradient to reflect palette
|
| 248 |
-
const header = d3.select(this).select('.palette-card__header');
|
| 249 |
-
const grad = `linear-gradient(90deg, ${data.join(',')})`;
|
| 250 |
-
header.style('background', grad);
|
| 251 |
-
|
| 252 |
-
// Update circular badge
|
| 253 |
-
const badge = d3.select(this).select('.palette-card__badge');
|
| 254 |
-
if (d.key === 'categorical') {
|
| 255 |
-
// Donut full hue circle for categorical
|
| 256 |
-
const hueCircle = 'conic-gradient(#f00 0%, #ff0 16.6%, #0f0 33.3%, #0ff 50%, #00f 66.6%, #f0f 83.3%, #f00 100%)';
|
| 257 |
-
badge.style('background', hueCircle);
|
| 258 |
-
// Clear previous markers
|
| 259 |
-
badge.selectAll('.badge-marker').remove();
|
| 260 |
-
// Place markers at palette hues
|
| 261 |
-
const radius = 21; // outer circle radius
|
| 262 |
-
const center = 22; // half of badge size (44)
|
| 263 |
-
data.forEach((hex) => {
|
| 264 |
-
const h = chroma(hex).get('hsl.h') || 0;
|
| 265 |
-
const angle = (h * Math.PI) / 180; // radians
|
| 266 |
-
const r = radius - 3; // adjust for 6px marker size
|
| 267 |
-
const x = center + r * Math.cos(angle - Math.PI / 2);
|
| 268 |
-
const y = center + r * Math.sin(angle - Math.PI / 2);
|
| 269 |
-
const mk = document.createElement('div');
|
| 270 |
-
mk.className = 'badge-marker';
|
| 271 |
-
mk.style.left = `${Math.round(x - 3)}px`;
|
| 272 |
-
mk.style.top = `${Math.round(y - 3)}px`;
|
| 273 |
-
mk.style.background = "rgba(255,255,255,0.9)";
|
| 274 |
-
badge.node().appendChild(mk);
|
| 275 |
-
});
|
| 276 |
-
// Inner hole to make it a donut
|
| 277 |
-
if (!badge.select('.badge-hole').node()) badge.append('div').attr('class', 'badge-hole');
|
| 278 |
-
} else {
|
| 279 |
-
// Linear gradient left->right for sequential/diverging
|
| 280 |
-
const linear = `linear-gradient(90deg, ${data.join(',')})`;
|
| 281 |
-
badge.style('background', linear);
|
| 282 |
-
badge.selectAll('.badge-marker').remove();
|
| 283 |
-
badge.selectAll('.badge-hole').remove();
|
| 284 |
-
}
|
| 285 |
});
|
| 286 |
-
};
|
| 287 |
-
|
| 288 |
-
// Hue slider behavior
|
| 289 |
-
let hue = 337; // initial
|
| 290 |
-
let isAdjusting = false; // only update theme while interacting
|
| 291 |
-
const setHue = (h) => { hue = (h + 360) % 360; const pct = hue / 360 * 100; if (knob) knob.style.left = pct + '%'; if (hueValue) hueValue.textContent = `H ${Math.round(hue)}°`; console.log('[palettes] setHue', hue, pct); renderPalettes(hue); };
|
| 292 |
-
const getHueFromEvent = (ev) => { const rect = slider.getBoundingClientRect(); const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX; const x = clientX - rect.left; const t = Math.max(0, Math.min(1, x / rect.width)); const h = t * 360; console.log('[palettes] getHueFromEvent', { clientX, left: rect.left, width: rect.width, t, h }); return h; };
|
| 293 |
-
const onDown = (ev) => { console.log('[palettes] onDown', ev.type); ev.preventDefault(); isAdjusting = true; const h0 = getHueFromEvent(ev); setHue(h0); bus.publish(instanceId, h0, true); const move = (e) => { e.preventDefault && e.preventDefault(); const hh = getHueFromEvent(e); setHue(hh); bus.publish(instanceId, hh, true); }; const up = () => { console.log('[palettes] onUp'); isAdjusting = false; bus.publish(instanceId, hue, false); window.removeEventListener('mousemove', move); window.removeEventListener('touchmove', move); window.removeEventListener('mouseup', up); window.removeEventListener('touchend', up); }; window.addEventListener('mousemove', move, { passive: false }); window.addEventListener('touchmove', move, { passive: false }); window.addEventListener('mouseup', up, { once: true }); window.addEventListener('touchend', up, { once: true }); };
|
| 294 |
-
if (slider) { slider.addEventListener('mousedown', onDown); slider.addEventListener('touchstart', onDown, { passive: false }); console.log('[palettes] listeners attached'); } else { console.warn('[palettes] slider not found'); }
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
if (document.getElementById('cvd-filters')) return;
|
| 302 |
-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| 303 |
-
svg.setAttribute('id', 'cvd-filters');
|
| 304 |
-
svg.setAttribute('width', '0'); svg.setAttribute('height', '0'); svg.style.position = 'absolute';
|
| 305 |
-
svg.innerHTML = `
|
| 306 |
-
<filter id="cvd-protanopia"><feColorMatrix type="matrix" values="0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"/></filter>
|
| 307 |
-
<filter id="cvd-deuteranopia"><feColorMatrix type="matrix" values="0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"/></filter>
|
| 308 |
-
<filter id="cvd-tritanopia"><feColorMatrix type="matrix" values="0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"/></filter>
|
| 309 |
-
<filter id="cvd-achromatopsia"><feColorMatrix type="matrix" values="0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0"/></filter>
|
| 310 |
-
`;
|
| 311 |
-
document.body.appendChild(svg);
|
| 312 |
-
};
|
| 313 |
-
injectFilters();
|
| 314 |
-
|
| 315 |
-
const applySimulation = (mode) => {
|
| 316 |
-
if (!grid) return;
|
| 317 |
-
if (!mode || mode === 'none') { grid.style.filter = 'none'; return; }
|
| 318 |
-
grid.style.filter = `url(#cvd-${mode})`;
|
| 319 |
};
|
| 320 |
-
if (simSelect) simSelect.addEventListener('change', () => applySimulation(simSelect.value));
|
| 321 |
-
|
| 322 |
-
console.log('[palettes] initial render');
|
| 323 |
-
// Initialize from shared hue without updating CSS variables
|
| 324 |
-
const shared = bus.get();
|
| 325 |
-
isAdjusting = false;
|
| 326 |
-
setHue(shared && typeof shared.hue === 'number' ? shared.hue : hue);
|
| 327 |
-
applySimulation('none');
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
| 331 |
};
|
| 332 |
|
| 333 |
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () =>
|
| 334 |
-
else
|
| 335 |
})();
|
| 336 |
</script>
|
| 337 |
|
|
|
|
| 1 |
<div class="palettes" style="width:100%; margin: 10px 0;">
|
| 2 |
<style>
|
| 3 |
+
.palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
| 4 |
+
.palettes .palette-card { position: relative; display: grid; grid-template-columns: 260px 1fr auto; align-items: center; gap: 14px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; }
|
| 5 |
+
/* removed circular badge */
|
| 6 |
+
.palettes .palette-card__swatches { display: flex; gap: 0; margin: 0; height: 40px; overflow: hidden; border-radius: 8px; }
|
| 7 |
+
.palettes .palette-card__swatches .sw { flex: 1 1 0; height: 100%; min-width: 0; border: none; }
|
| 8 |
+
.palettes .palette-card__content { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; justify-content: center; min-width: 0; padding-left: 12px; border-left: 1px solid var(--border-color); }
|
| 9 |
+
.palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-end; justify-self: end; }
|
| 10 |
+
.palettes .copy-btn { margin: 0; padding: 6px 12px; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color)!important; font-size: 12px; cursor: pointer; transition: background .18s ease, color .18s ease, border-color .18s ease; }
|
| 11 |
+
.palettes .copy-btn:hover { background: var(--primary-color); color: var(--on-primary)!important; border-color: transparent; }
|
| 12 |
+
.palettes .copy-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
| 13 |
+
@media (max-width: 640px) {
|
| 14 |
+
.palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; }
|
| 15 |
+
.palettes .palette-card__swatches { height: 36px; border-radius: 8px; }
|
| 16 |
+
.palettes .palette-card__content { border-left: none; padding-left: 0; }
|
| 17 |
+
.palettes .palette-card__actions { justify-self: start; }
|
| 18 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
<div class="palettes__grid"></div>
|
| 21 |
</div>
|
| 22 |
<script>
|
| 23 |
(() => {
|
|
|
|
| 24 |
const loadScript = (id, src, onload, onerror) => {
|
| 25 |
let s = document.getElementById(id);
|
| 26 |
if (s) { return onload && onload(); }
|
| 27 |
+
s = document.createElement('script'); s.id = id; s.src = src; s.async = true;
|
|
|
|
|
|
|
|
|
|
| 28 |
if (onload) s.addEventListener('load', onload, { once: true });
|
| 29 |
if (onerror) s.addEventListener('error', onerror, { once: true });
|
| 30 |
document.head.appendChild(s);
|
| 31 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
const ensureChroma = (next) => {
|
| 33 |
if (window.chroma) return next();
|
| 34 |
+
const tryReady = () => { if (window.chroma) next(); else setTimeout(tryReady, 25); };
|
| 35 |
+
const existing = document.getElementById('chroma-cdn') || document.getElementById('chroma-cdn-fallback');
|
| 36 |
+
if (existing) { tryReady(); return; }
|
| 37 |
+
loadScript('chroma-cdn', 'https://unpkg.com/[email protected]/dist/chroma.min.js', tryReady, () => {
|
| 38 |
+
loadScript('chroma-cdn-fallback', 'https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.4.2/chroma.min.js', tryReady);
|
| 39 |
});
|
| 40 |
};
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
const cards = [
|
| 43 |
+
{ key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors. <strong>Up to 6</strong>.', generator: (baseHex) => {
|
| 44 |
+
const base = chroma(baseHex);
|
| 45 |
+
const lc = base.lch();
|
| 46 |
+
const baseH = base.get('hsl.h') || 0;
|
| 47 |
+
const L = Math.max(40, Math.min(85, lc[0] || 70));
|
| 48 |
+
const C = Math.max(40, Math.min(90, lc[1] || 80));
|
| 49 |
+
const offsets = [180, 120, -120, 60, -60];
|
| 50 |
+
const rest = offsets.map(off => chroma.lch(L, C, (baseH + off + 360) % 360).hex());
|
| 51 |
+
return [base.hex(), ...rest];
|
| 52 |
+
}},
|
| 53 |
+
{ key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>light to dark</strong>; ideal for <strong>heatmaps</strong>.', generator: (baseHex) => {
|
| 54 |
+
const c = chroma(baseHex).saturate(0.3);
|
| 55 |
+
return chroma.scale([c.brighten(2), c, c.darken(2)]).mode('lab').correctLightness(true).colors(6);
|
| 56 |
+
}},
|
| 57 |
+
{ key: 'diverging', title: 'Diverging', desc: 'For <strong>centered ranges</strong> with <strong>two extremes</strong> (e.g., negatives/positives) around a <strong>baseline</strong>.', generator: (baseHex) => {
|
| 58 |
+
const baseH = chroma(baseHex).get('hsl.h');
|
| 59 |
+
const compH = (baseH + 180) % 360;
|
| 60 |
+
const left = chroma.hsl(compH, 0.75, 0.55);
|
| 61 |
+
const right = chroma.hsl(baseH, 0.75, 0.55);
|
| 62 |
+
const center = '#ffffff';
|
| 63 |
+
const leftRamp = chroma.scale([left, center]).mode('lch').correctLightness(true).colors(4);
|
| 64 |
+
const rightRamp = chroma.scale([center, right]).mode('lch').correctLightness(true).colors(4);
|
| 65 |
+
return [leftRamp[0], leftRamp[1], leftRamp[2], rightRamp[1], rightRamp[2], rightRamp[3]];
|
| 66 |
+
}}
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
const getCssPrimary = () => {
|
| 70 |
+
try { return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); } catch { return ''; }
|
| 71 |
+
};
|
| 72 |
|
| 73 |
+
const render = () => {
|
|
|
|
| 74 |
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
|
| 75 |
const root = mount && mount.closest('.palettes') ? mount.closest('.palettes') : document.querySelector('.palettes');
|
| 76 |
+
if (!root) return;
|
|
|
|
| 77 |
const grid = root.querySelector('.palettes__grid');
|
| 78 |
+
if (!grid) return;
|
| 79 |
+
grid.innerHTML = '';
|
| 80 |
+
const css = getCssPrimary();
|
| 81 |
+
const baseHex = css && /^#|rgb|hsl/i.test(css) ? chroma(css).hex() : '#E889AB';
|
| 82 |
+
|
| 83 |
+
cards.forEach((c) => {
|
| 84 |
+
const card = document.createElement('div'); card.className = 'palette-card';
|
| 85 |
+
const sw = document.createElement('div'); sw.className = 'palette-card__swatches';
|
| 86 |
+
const colors = c.generator(baseHex).slice(0, 6);
|
| 87 |
+
colors.forEach(col => { const d = document.createElement('div'); d.className = 'sw'; d.style.background = col; sw.appendChild(d); });
|
| 88 |
+
|
| 89 |
+
const content = document.createElement('div'); content.className = 'palette-card__content';
|
| 90 |
+
const title = document.createElement('div'); title.className = 'palette-card__title'; title.style.textAlign = 'left'; title.style.fontWeight = '800'; title.style.fontSize = '15px'; title.textContent = c.title;
|
| 91 |
+
const desc = document.createElement('div'); desc.className = 'palette-card__desc'; desc.style.textAlign = 'left'; desc.style.color = 'var(--muted-color)'; desc.style.lineHeight = '1.5'; desc.style.fontSize = '12px'; desc.innerHTML = c.desc;
|
| 92 |
+
const actions = document.createElement('div'); actions.className = 'palette-card__actions';
|
| 93 |
+
const btn = document.createElement('button'); btn.className = 'copy-btn'; btn.textContent = 'Copy';
|
| 94 |
+
btn.addEventListener('click', async () => {
|
| 95 |
+
const json = JSON.stringify(colors, null, 2);
|
| 96 |
+
try { await navigator.clipboard.writeText(json); const old = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = old, 900); } catch { window.prompt('Copy palette', json); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
content.appendChild(title); content.appendChild(desc);
|
| 100 |
+
actions.appendChild(btn);
|
| 101 |
+
card.appendChild(sw); card.appendChild(content); card.appendChild(actions);
|
| 102 |
+
grid.appendChild(card);
|
| 103 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
const bootstrap = () => {
|
| 107 |
+
render();
|
| 108 |
+
const mo = new MutationObserver(() => render());
|
| 109 |
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
|
| 110 |
};
|
| 111 |
|
| 112 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureChroma(bootstrap), { once: true });
|
| 113 |
+
else ensureChroma(bootstrap);
|
| 114 |
})();
|
| 115 |
</script>
|
| 116 |
|
app/src/pages/index.astro
CHANGED
|
@@ -82,11 +82,11 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
|
|
| 82 |
<section class="content-grid">
|
| 83 |
<aside class="toc">
|
| 84 |
<div class="title">Table of Contents</div>
|
| 85 |
-
<div id="toc-placeholder"></div>
|
| 86 |
</aside>
|
| 87 |
<details class="toc-mobile">
|
| 88 |
<summary>Table of Contents</summary>
|
| 89 |
-
<div id="toc-mobile-placeholder"></div>
|
| 90 |
</details>
|
| 91 |
<main>
|
| 92 |
<Article />
|
|
@@ -193,24 +193,64 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
|
|
| 193 |
<script>
|
| 194 |
// Build TOC from article headings (h2/h3/h4) and render into the sticky aside
|
| 195 |
const buildTOC = () => {
|
| 196 |
-
const holder = document.getElementById('toc-placeholder');
|
| 197 |
-
const holderMobile = document.getElementById('toc-mobile-placeholder');
|
| 198 |
-
|
|
|
|
|
|
|
| 199 |
const articleRoot = document.querySelector('section.content-grid main');
|
| 200 |
if (!articleRoot) return;
|
| 201 |
const headings = articleRoot.querySelectorAll('h2, h3, h4');
|
| 202 |
if (!headings.length) return;
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
const nav = document.createElement('nav');
|
| 205 |
let ulStack = [document.createElement('ul')];
|
| 206 |
nav.appendChild(ulStack[0]);
|
| 207 |
|
| 208 |
const levelOf = (tag) => tag === 'H2' ? 2 : tag === 'H3' ? 3 : 4;
|
| 209 |
let prev = 2;
|
| 210 |
-
|
| 211 |
const lvl = levelOf(h.tagName);
|
| 212 |
-
// ensure id exists
|
| 213 |
-
if (!h.id) h.id = h.textContent.toLowerCase().replace(/\s+/g, '_');
|
| 214 |
// adjust depth
|
| 215 |
while (lvl > prev) { const ul = document.createElement('ul'); ulStack[ulStack.length-1].lastElementChild?.appendChild(ul); ulStack.push(ul); prev++; }
|
| 216 |
while (lvl < prev) { ulStack.pop(); prev--; }
|
|
@@ -230,11 +270,11 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
|
|
| 230 |
...(holderMobile ? holderMobile.querySelectorAll('a') : [])
|
| 231 |
];
|
| 232 |
const onScroll = () => {
|
| 233 |
-
for (let i =
|
| 234 |
-
const top =
|
| 235 |
if (top - 60 <= 0) {
|
| 236 |
links.forEach(l => l.classList.remove('active'));
|
| 237 |
-
const id = '#' +
|
| 238 |
const actives = Array.from(links).filter(l => l.getAttribute('href') === id);
|
| 239 |
actives.forEach(a => a.classList.add('active'));
|
| 240 |
break;
|
|
|
|
| 82 |
<section class="content-grid">
|
| 83 |
<aside class="toc">
|
| 84 |
<div class="title">Table of Contents</div>
|
| 85 |
+
<div id="article-toc-placeholder"></div>
|
| 86 |
</aside>
|
| 87 |
<details class="toc-mobile">
|
| 88 |
<summary>Table of Contents</summary>
|
| 89 |
+
<div id="article-toc-mobile-placeholder"></div>
|
| 90 |
</details>
|
| 91 |
<main>
|
| 92 |
<Article />
|
|
|
|
| 193 |
<script>
|
| 194 |
// Build TOC from article headings (h2/h3/h4) and render into the sticky aside
|
| 195 |
const buildTOC = () => {
|
| 196 |
+
const holder = document.getElementById('article-toc-placeholder');
|
| 197 |
+
const holderMobile = document.getElementById('article-toc-mobile-placeholder');
|
| 198 |
+
// Always rebuild TOC to avoid stale entries
|
| 199 |
+
if (holder) holder.innerHTML = '';
|
| 200 |
+
if (holderMobile) holderMobile.innerHTML = '';
|
| 201 |
const articleRoot = document.querySelector('section.content-grid main');
|
| 202 |
if (!articleRoot) return;
|
| 203 |
const headings = articleRoot.querySelectorAll('h2, h3, h4');
|
| 204 |
if (!headings.length) return;
|
| 205 |
|
| 206 |
+
// Filter out headings that should not appear in TOC
|
| 207 |
+
const normalize = (s) => String(s || '')
|
| 208 |
+
.toLowerCase()
|
| 209 |
+
.replace(/[^a-z0-9]+/g, ' ')
|
| 210 |
+
.trim();
|
| 211 |
+
const isTocLabel = (s) => /^(table\s+of\s+contents?)$|^toc$/i.test(String(s || '').replace(/[^a-zA-Z0-9]+/g, ' ').trim());
|
| 212 |
+
const shouldSkip = (h) => {
|
| 213 |
+
const t = h.textContent || '';
|
| 214 |
+
const id = String(h.id || '');
|
| 215 |
+
const slug = normalize(t).replace(/\s+/g, '_');
|
| 216 |
+
if (isTocLabel(t)) return true;
|
| 217 |
+
if (isTocLabel(id.replace(/[_-]+/g, ' '))) return true;
|
| 218 |
+
if (isTocLabel(slug.replace(/[_-]+/g, ' '))) return true;
|
| 219 |
+
return false;
|
| 220 |
+
};
|
| 221 |
+
const headingsArr = Array.from(headings).filter(h => !shouldSkip(h));
|
| 222 |
+
if (!headingsArr.length) return;
|
| 223 |
+
|
| 224 |
+
// Ensure unique ids for headings (deduplicate duplicates)
|
| 225 |
+
const usedIds = new Set<string>();
|
| 226 |
+
const slugify = (s: string) => String(s || '')
|
| 227 |
+
.toLowerCase()
|
| 228 |
+
.trim()
|
| 229 |
+
.replace(/\s+/g, '_')
|
| 230 |
+
.replace(/[^a-z0-9_\-]/g, '');
|
| 231 |
+
headingsArr.forEach((h) => {
|
| 232 |
+
let id = (h.id || '').trim();
|
| 233 |
+
if (!id) {
|
| 234 |
+
const base = slugify(h.textContent || '');
|
| 235 |
+
id = base || 'section';
|
| 236 |
+
}
|
| 237 |
+
let candidate = id;
|
| 238 |
+
let n = 2;
|
| 239 |
+
while (usedIds.has(candidate)) {
|
| 240 |
+
candidate = `${id}-${n++}`;
|
| 241 |
+
}
|
| 242 |
+
if (h.id !== candidate) h.id = candidate;
|
| 243 |
+
usedIds.add(candidate);
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
const nav = document.createElement('nav');
|
| 247 |
let ulStack = [document.createElement('ul')];
|
| 248 |
nav.appendChild(ulStack[0]);
|
| 249 |
|
| 250 |
const levelOf = (tag) => tag === 'H2' ? 2 : tag === 'H3' ? 3 : 4;
|
| 251 |
let prev = 2;
|
| 252 |
+
headingsArr.forEach((h) => {
|
| 253 |
const lvl = levelOf(h.tagName);
|
|
|
|
|
|
|
| 254 |
// adjust depth
|
| 255 |
while (lvl > prev) { const ul = document.createElement('ul'); ulStack[ulStack.length-1].lastElementChild?.appendChild(ul); ulStack.push(ul); prev++; }
|
| 256 |
while (lvl < prev) { ulStack.pop(); prev--; }
|
|
|
|
| 270 |
...(holderMobile ? holderMobile.querySelectorAll('a') : [])
|
| 271 |
];
|
| 272 |
const onScroll = () => {
|
| 273 |
+
for (let i = headingsArr.length - 1; i >= 0; i--) {
|
| 274 |
+
const top = headingsArr[i].getBoundingClientRect().top;
|
| 275 |
if (top - 60 <= 0) {
|
| 276 |
links.forEach(l => l.classList.remove('active'));
|
| 277 |
+
const id = '#' + headingsArr[i].id;
|
| 278 |
const actives = Array.from(links).filter(l => l.getAttribute('href') === id);
|
| 279 |
actives.forEach(a => a.classList.add('active'));
|
| 280 |
break;
|
app/src/styles/_base.css
CHANGED
|
@@ -42,8 +42,8 @@ html { font-size: 14px; line-height: 1.6; }
|
|
| 42 |
margin: var(--spacing-8) 0 var(--spacing-4);
|
| 43 |
}
|
| 44 |
|
| 45 |
-
.content-grid main a { color: var(--primary); text-decoration: none; border-bottom: 1px solid var(--link-underline); }
|
| 46 |
-
.content-grid main a:hover { color: var(--primary-hover); border-bottom: 1px solid var(--link-underline-hover); }
|
| 47 |
|
| 48 |
/* Do not underline heading links inside the article (not the TOC) */
|
| 49 |
.content-grid main h2 a,
|
|
@@ -178,7 +178,7 @@ figcaption { text-align: left; font-size: 0.9rem; color: var(--muted-color); mar
|
|
| 178 |
/* ============================================================================ */
|
| 179 |
button, .button {
|
| 180 |
appearance: none;
|
| 181 |
-
background: linear-gradient(15deg, var(--primary) 0%, var(--primary-hover) 35%);
|
| 182 |
color: white!important;
|
| 183 |
border: 1px solid transparent;
|
| 184 |
border-radius: 6px;
|
|
@@ -239,6 +239,30 @@ button:disabled, .button:disabled {
|
|
| 239 |
--link-underline: rgba(0,0,0,.3);
|
| 240 |
--link-underline-hover: rgba(0,0,0,.4);
|
| 241 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
.muted {
|
|
|
|
| 42 |
margin: var(--spacing-8) 0 var(--spacing-4);
|
| 43 |
}
|
| 44 |
|
| 45 |
+
.content-grid main a { color: var(--primary-color); text-decoration: none; border-bottom: 1px solid var(--link-underline); }
|
| 46 |
+
.content-grid main a:hover { color: var(--primary-color-hover); border-bottom: 1px solid var(--link-underline-hover); }
|
| 47 |
|
| 48 |
/* Do not underline heading links inside the article (not the TOC) */
|
| 49 |
.content-grid main h2 a,
|
|
|
|
| 178 |
/* ============================================================================ */
|
| 179 |
button, .button {
|
| 180 |
appearance: none;
|
| 181 |
+
background: linear-gradient(15deg, var(--primary-color) 0%, var(--primary-color-hover) 35%);
|
| 182 |
color: white!important;
|
| 183 |
border: 1px solid transparent;
|
| 184 |
border-radius: 6px;
|
|
|
|
| 239 |
--link-underline: rgba(0,0,0,.3);
|
| 240 |
--link-underline-hover: rgba(0,0,0,.4);
|
| 241 |
}
|
| 242 |
+
|
| 243 |
+
/* Force single column to reduce widows/orphans and awkward breaks */
|
| 244 |
+
.content-grid { grid-template-columns: 1fr !important; }
|
| 245 |
+
.toc, .right-aside, .toc-mobile { display: none !important; }
|
| 246 |
+
main > nav:first-of-type { display: none !important; }
|
| 247 |
+
|
| 248 |
+
/* Avoid page breaks inside complex visual blocks */
|
| 249 |
+
.hero,
|
| 250 |
+
.hero-banner,
|
| 251 |
+
.d3-galaxy,
|
| 252 |
+
.d3-galaxy svg,
|
| 253 |
+
.plot-card,
|
| 254 |
+
.js-plotly-plot,
|
| 255 |
+
figure,
|
| 256 |
+
pre,
|
| 257 |
+
table,
|
| 258 |
+
blockquote,
|
| 259 |
+
.wide,
|
| 260 |
+
.full-bleed {
|
| 261 |
+
break-inside: avoid;
|
| 262 |
+
page-break-inside: avoid;
|
| 263 |
+
}
|
| 264 |
+
/* Prefer keeping header+lead together */
|
| 265 |
+
.hero { page-break-after: avoid; }
|
| 266 |
}
|
| 267 |
|
| 268 |
.muted {
|
app/src/styles/_variables.css
CHANGED
|
@@ -9,8 +9,8 @@
|
|
| 9 |
--neutral-200: rgb(245, 245, 245);
|
| 10 |
|
| 11 |
/* Primary brand color */
|
| 12 |
-
--primary: rgb(232, 137, 171);
|
| 13 |
-
--primary-hover: rgb(212, 126, 156);
|
| 14 |
--on-primary: #ffffff;
|
| 15 |
|
| 16 |
--text-color: rgba(0,0,0,.85);
|
|
@@ -20,8 +20,8 @@
|
|
| 20 |
/* Light surfaces & links */
|
| 21 |
--surface-bg: #fafafa;
|
| 22 |
--code-bg: #f6f8fa;
|
| 23 |
-
--link-underline: var(--primary); /* based on --primary */
|
| 24 |
-
--link-underline-hover: var(--primary-hover);
|
| 25 |
|
| 26 |
--spacing-1: 8px;
|
| 27 |
--spacing-2: 12px;
|
|
@@ -42,8 +42,8 @@
|
|
| 42 |
--surface-bg: #12151b;
|
| 43 |
--code-bg: #12151b;
|
| 44 |
/* Primary in dark mode */
|
| 45 |
-
--primary: rgb(232, 137, 171);
|
| 46 |
-
--primary-hover: rgb(212, 126, 156);
|
| 47 |
--on-primary: #0f1115;
|
| 48 |
|
| 49 |
color-scheme: dark;
|
|
|
|
| 9 |
--neutral-200: rgb(245, 245, 245);
|
| 10 |
|
| 11 |
/* Primary brand color */
|
| 12 |
+
--primary-color: rgb(232, 137, 171);
|
| 13 |
+
--primary-color-hover: rgb(212, 126, 156);
|
| 14 |
--on-primary: #ffffff;
|
| 15 |
|
| 16 |
--text-color: rgba(0,0,0,.85);
|
|
|
|
| 20 |
/* Light surfaces & links */
|
| 21 |
--surface-bg: #fafafa;
|
| 22 |
--code-bg: #f6f8fa;
|
| 23 |
+
--link-underline: var(--primary-color); /* based on --primary-color */
|
| 24 |
+
--link-underline-hover: var(--primary-color-hover);
|
| 25 |
|
| 26 |
--spacing-1: 8px;
|
| 27 |
--spacing-2: 12px;
|
|
|
|
| 42 |
--surface-bg: #12151b;
|
| 43 |
--code-bg: #12151b;
|
| 44 |
/* Primary in dark mode */
|
| 45 |
+
--primary-color: rgb(232, 137, 171);
|
| 46 |
+
--primary-color-hover: rgb(212, 126, 156);
|
| 47 |
--on-primary: #0f1115;
|
| 48 |
|
| 49 |
color-scheme: dark;
|
app/src/styles/components/_poltly.css
CHANGED
|
@@ -13,8 +13,8 @@
|
|
| 13 |
.plotly_input_container > select { padding: 2px 4px; line-height: 1.5em; text-align: center; border-radius: 4px; font-size: 12px; background-color: var(--neutral-200); outline: none; border: 1px solid var(--neutral-300); }
|
| 14 |
.plotly_slider { display: flex; align-items: center; gap: 10px; }
|
| 15 |
.plotly_slider > input[type="range"] { -webkit-appearance: none; appearance: none; height: 2px; background: var(--neutral-400); border-radius: 5px; outline: none; }
|
| 16 |
-
.plotly_slider > input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--primary); cursor: pointer; }
|
| 17 |
-
.plotly_slider > input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--primary); cursor: pointer; }
|
| 18 |
.plotly_slider > span { font-size: 14px; line-height: 1.6em; min-width: 16px; }
|
| 19 |
|
| 20 |
/* ---------------------------------------------------------------------------- */
|
|
|
|
| 13 |
.plotly_input_container > select { padding: 2px 4px; line-height: 1.5em; text-align: center; border-radius: 4px; font-size: 12px; background-color: var(--neutral-200); outline: none; border: 1px solid var(--neutral-300); }
|
| 14 |
.plotly_slider { display: flex; align-items: center; gap: 10px; }
|
| 15 |
.plotly_slider > input[type="range"] { -webkit-appearance: none; appearance: none; height: 2px; background: var(--neutral-400); border-radius: 5px; outline: none; }
|
| 16 |
+
.plotly_slider > input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--primary-color); cursor: pointer; }
|
| 17 |
+
.plotly_slider > input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--primary-color); cursor: pointer; }
|
| 18 |
.plotly_slider > span { font-size: 14px; line-height: 1.6em; min-width: 16px; }
|
| 19 |
|
| 20 |
/* ---------------------------------------------------------------------------- */
|