update
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +6 -6
- CHANGELOG.md +118 -0
- CONTRIBUTING.md +196 -0
- Dockerfile +15 -21
- LICENSE +33 -0
- app/astro.config.mjs +4 -2
- app/package.json +0 -0
- app/public/scripts/color-palettes.js +82 -44
- app/scripts/notion-importer/.notion-to-md/media/27877f1c-9c9d-804d-9c82-f7b3905578ff_media.json +3 -198
- app/scripts/notion-importer/README.md +3 -33
- app/scripts/notion-importer/env.example +72 -2
- app/scripts/notion-importer/index.mjs +8 -80
- app/scripts/notion-importer/input/pages.json +3 -9
- app/scripts/notion-importer/mdx-converter.mjs +12 -12
- app/scripts/notion-importer/notion-converter.mjs +2 -2
- app/scripts/sync-template.mjs +361 -0
- app/src/components/Glossary.astro +336 -0
- app/src/components/Hero.astro +312 -17
- app/src/components/HtmlEmbed.astro +8 -3
- app/src/components/Image.astro +508 -0
- app/src/components/Quote.astro +124 -0
- app/src/components/Reference.astro +45 -0
- app/src/components/Sidenote.astro +77 -15
- app/src/components/Stack.astro +161 -0
- app/src/components/demo/ColorPicker.astro +633 -0
- app/src/components/demo/Palettes.astro +596 -0
- app/src/components/trackio/Trackio.svelte +500 -259
- app/src/components/trackio/TrackioWrapper.astro +510 -0
- app/src/components/trackio/core/adaptive-sampler.js +9 -9
- app/src/content/embeds/banner.html +633 -0
- app/src/content/{embeds2 → embeds}/d3-bar.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-benchmark.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-confusion-matrix.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-evals-after-fix.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-evals-tpbug.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-line-quad.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-line.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-matrix.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-neural-network.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-pie-quad.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-pie.html +0 -0
- app/src/content/{embeds2 → embeds}/d3-scatter.html +0 -0
- app/src/content/{embeds2 → embeds}/demo/color-picker.html +0 -0
- app/src/content/{embeds2 → embeds}/demo/content-structure.html +0 -0
- app/src/content/{embeds2 → embeds}/demo/palettes.html +0 -0
- app/src/content/{embeds2 → embeds}/original_embeds/plotly/banner.py +0 -0
- app/src/content/{embeds2 → embeds}/original_embeds/plotly/bar.py +0 -0
- app/src/content/{embeds2 → embeds}/original_embeds/plotly/heatmap.py +0 -0
- app/src/content/{embeds2 → embeds}/original_embeds/plotly/line.py +0 -0
- app/src/content/{embeds2 → embeds}/original_embeds/plotly/poetry.lock +0 -0
.gitattributes
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
-
# Images et médias
|
| 2 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
|
| 7 |
-
# Vidéos et audio
|
| 8 |
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.mov filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.avi filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
|
| 14 |
-
*.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 6 |
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.mov filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.avi filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 9 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.csv filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.json filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
# the package and package lock should not be tracked
|
| 13 |
+
package.json -filter -diff -merge text
|
| 14 |
+
package-lock.json -filter -diff -merge text
|
CHANGELOG.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog
|
| 2 |
+
|
| 3 |
+
All notable changes to the Research Article Template will be documented in this file.
|
| 4 |
+
|
| 5 |
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
| 6 |
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
| 7 |
+
|
| 8 |
+
## [Unreleased]
|
| 9 |
+
|
| 10 |
+
### Added
|
| 11 |
+
- Initial open source release
|
| 12 |
+
- Comprehensive documentation
|
| 13 |
+
- Contributing guidelines
|
| 14 |
+
- License file
|
| 15 |
+
|
| 16 |
+
## [1.0.0] - 2024-12-19
|
| 17 |
+
|
| 18 |
+
### Added
|
| 19 |
+
- **Core Features**:
|
| 20 |
+
- Markdown/MDX-based writing system
|
| 21 |
+
- KaTeX mathematical notation support
|
| 22 |
+
- Syntax highlighting for code blocks
|
| 23 |
+
- Academic citations with BibTeX integration
|
| 24 |
+
- Footnotes and sidenotes system
|
| 25 |
+
- Auto-generated table of contents
|
| 26 |
+
- Interactive Mermaid diagrams
|
| 27 |
+
- Plotly.js and D3.js integration
|
| 28 |
+
- HTML embed support
|
| 29 |
+
- Gradio app embedding
|
| 30 |
+
- Dataviz color palettes
|
| 31 |
+
- Image optimization
|
| 32 |
+
- SEO-friendly structure
|
| 33 |
+
- Automatic PDF export
|
| 34 |
+
- Dark/light theme toggle
|
| 35 |
+
- Mobile-responsive design
|
| 36 |
+
- LaTeX import functionality
|
| 37 |
+
- Template synchronization system
|
| 38 |
+
|
| 39 |
+
- **Components**:
|
| 40 |
+
- Figure component with captions
|
| 41 |
+
- MultiFigure for image galleries
|
| 42 |
+
- Note component with variants
|
| 43 |
+
- Quote component
|
| 44 |
+
- Accordion for collapsible content
|
| 45 |
+
- Sidenote component
|
| 46 |
+
- Table of Contents
|
| 47 |
+
- Theme Toggle
|
| 48 |
+
- HTML Embed
|
| 49 |
+
- Raw HTML support
|
| 50 |
+
- SEO component
|
| 51 |
+
- Hero section
|
| 52 |
+
- Footer
|
| 53 |
+
- Full-width and wide layouts
|
| 54 |
+
|
| 55 |
+
- **Build System**:
|
| 56 |
+
- Astro 4.10.0 integration
|
| 57 |
+
- PostCSS with custom media queries
|
| 58 |
+
- Automatic compression
|
| 59 |
+
- Docker support
|
| 60 |
+
- Nginx configuration
|
| 61 |
+
- Git LFS support
|
| 62 |
+
|
| 63 |
+
- **Scripts**:
|
| 64 |
+
- PDF export functionality
|
| 65 |
+
- LaTeX to MDX conversion
|
| 66 |
+
- Template synchronization
|
| 67 |
+
- Font SVG generation
|
| 68 |
+
- TrackIO data generation
|
| 69 |
+
|
| 70 |
+
- **Documentation**:
|
| 71 |
+
- Getting started guide
|
| 72 |
+
- Writing best practices
|
| 73 |
+
- Component reference
|
| 74 |
+
- LaTeX conversion guide
|
| 75 |
+
- Interactive examples
|
| 76 |
+
|
| 77 |
+
### Technical Details
|
| 78 |
+
- **Framework**: Astro 4.10.0
|
| 79 |
+
- **Styling**: PostCSS with custom properties
|
| 80 |
+
- **Math**: KaTeX 0.16.22
|
| 81 |
+
- **Charts**: Plotly.js 3.1.0, D3.js 7.9.0
|
| 82 |
+
- **Diagrams**: Mermaid 11.10.1
|
| 83 |
+
- **Node.js**: >=20.0.0
|
| 84 |
+
- **License**: CC-BY-4.0
|
| 85 |
+
|
| 86 |
+
### Browser Support
|
| 87 |
+
- Chrome (latest)
|
| 88 |
+
- Firefox (latest)
|
| 89 |
+
- Safari (latest)
|
| 90 |
+
- Edge (latest)
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## Version History
|
| 95 |
+
|
| 96 |
+
- **1.0.0**: Initial stable release with full feature set
|
| 97 |
+
- **0.0.1**: Development version (pre-release)
|
| 98 |
+
|
| 99 |
+
## Migration Guide
|
| 100 |
+
|
| 101 |
+
### From 0.0.1 to 1.0.0
|
| 102 |
+
|
| 103 |
+
This is the first stable release. No breaking changes from the development version.
|
| 104 |
+
|
| 105 |
+
### Updating Your Project
|
| 106 |
+
|
| 107 |
+
Use the template synchronization system to update:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
npm run sync:template -- --dry-run # Preview changes
|
| 111 |
+
npm run sync:template # Apply updates
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Support
|
| 115 |
+
|
| 116 |
+
- **Documentation**: [Hugging Face Space](https://huggingface.co/spaces/tfrere/research-article-template)
|
| 117 |
+
- **Issues**: [Community Discussions](https://huggingface.co/spaces/tfrere/research-article-template/discussions)
|
| 118 |
+
- **Contact**: [@tfrere](https://huggingface.co/tfrere)
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to Research Article Template
|
| 2 |
+
|
| 3 |
+
Thank you for your interest in contributing to the Research Article Template! This document provides guidelines and information for contributors.
|
| 4 |
+
|
| 5 |
+
## 🤝 How to Contribute
|
| 6 |
+
|
| 7 |
+
### Reporting Issues
|
| 8 |
+
|
| 9 |
+
Before creating an issue, please:
|
| 10 |
+
1. **Search existing issues** to avoid duplicates
|
| 11 |
+
2. **Use the issue template** when available
|
| 12 |
+
3. **Provide detailed information**:
|
| 13 |
+
- Clear description of the problem
|
| 14 |
+
- Steps to reproduce
|
| 15 |
+
- Expected vs actual behavior
|
| 16 |
+
- Environment details (OS, Node.js version, browser)
|
| 17 |
+
- Screenshots if applicable
|
| 18 |
+
|
| 19 |
+
### Suggesting Features
|
| 20 |
+
|
| 21 |
+
We welcome feature suggestions! Please:
|
| 22 |
+
1. **Check existing discussions** first
|
| 23 |
+
2. **Describe the use case** clearly
|
| 24 |
+
3. **Explain the benefits** for the community
|
| 25 |
+
4. **Consider implementation complexity**
|
| 26 |
+
|
| 27 |
+
### Code Contributions
|
| 28 |
+
|
| 29 |
+
#### Getting Started
|
| 30 |
+
|
| 31 |
+
1. **Fork the repository** on Hugging Face
|
| 32 |
+
2. **Clone your fork**:
|
| 33 |
+
```bash
|
| 34 |
+
git clone git@hf.co:spaces/<your-username>/research-article-template
|
| 35 |
+
cd research-article-template
|
| 36 |
+
```
|
| 37 |
+
3. **Install dependencies**:
|
| 38 |
+
```bash
|
| 39 |
+
cd app
|
| 40 |
+
npm install
|
| 41 |
+
```
|
| 42 |
+
4. **Create a feature branch**:
|
| 43 |
+
```bash
|
| 44 |
+
git checkout -b feature/your-feature-name
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
#### Development Workflow
|
| 48 |
+
|
| 49 |
+
1. **Make your changes** following our coding standards
|
| 50 |
+
2. **Test thoroughly**:
|
| 51 |
+
```bash
|
| 52 |
+
npm run dev # Test locally
|
| 53 |
+
npm run build # Ensure build works
|
| 54 |
+
```
|
| 55 |
+
3. **Update documentation** if needed
|
| 56 |
+
4. **Commit with clear messages**:
|
| 57 |
+
```bash
|
| 58 |
+
git commit -m "feat: add new component for interactive charts"
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
#### Pull Request Process
|
| 62 |
+
|
| 63 |
+
1. **Push your branch**:
|
| 64 |
+
```bash
|
| 65 |
+
git push origin feature/your-feature-name
|
| 66 |
+
```
|
| 67 |
+
2. **Create a Pull Request** with:
|
| 68 |
+
- Clear title and description
|
| 69 |
+
- Reference related issues
|
| 70 |
+
- Screenshots for UI changes
|
| 71 |
+
- Testing instructions
|
| 72 |
+
|
| 73 |
+
## 📋 Coding Standards
|
| 74 |
+
|
| 75 |
+
### Code Style
|
| 76 |
+
|
| 77 |
+
- **Use Prettier** for consistent formatting
|
| 78 |
+
- **Follow existing patterns** in the codebase
|
| 79 |
+
- **Write clear, self-documenting code**
|
| 80 |
+
- **Add comments** for complex logic
|
| 81 |
+
- **Use meaningful variable names**
|
| 82 |
+
|
| 83 |
+
### File Organization
|
| 84 |
+
|
| 85 |
+
- **Components**: Place in `src/components/`
|
| 86 |
+
- **Styles**: Use CSS modules or component-scoped styles
|
| 87 |
+
- **Assets**: Organize in `src/content/assets/`
|
| 88 |
+
- **Documentation**: Update relevant `.mdx` files
|
| 89 |
+
|
| 90 |
+
### Commit Message Format
|
| 91 |
+
|
| 92 |
+
We follow [Conventional Commits](https://www.conventionalcommits.org/):
|
| 93 |
+
|
| 94 |
+
```
|
| 95 |
+
type(scope): description
|
| 96 |
+
|
| 97 |
+
feat: add new interactive chart component
|
| 98 |
+
fix: resolve mobile layout issues
|
| 99 |
+
docs: update installation instructions
|
| 100 |
+
style: improve button hover states
|
| 101 |
+
refactor: simplify component structure
|
| 102 |
+
test: add unit tests for utility functions
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
| 106 |
+
|
| 107 |
+
## 🧪 Testing
|
| 108 |
+
|
| 109 |
+
### Manual Testing
|
| 110 |
+
|
| 111 |
+
Before submitting:
|
| 112 |
+
- [ ] Test on different screen sizes
|
| 113 |
+
- [ ] Verify dark/light theme compatibility
|
| 114 |
+
- [ ] Check browser compatibility (Chrome, Firefox, Safari)
|
| 115 |
+
- [ ] Test with different content types
|
| 116 |
+
- [ ] Ensure accessibility standards
|
| 117 |
+
|
| 118 |
+
### Automated Testing
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
# Run build to catch errors
|
| 122 |
+
npm run build
|
| 123 |
+
|
| 124 |
+
# Test PDF export
|
| 125 |
+
npm run export:pdf
|
| 126 |
+
|
| 127 |
+
# Test LaTeX conversion
|
| 128 |
+
npm run latex:convert
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 📚 Documentation
|
| 132 |
+
|
| 133 |
+
### Writing Guidelines
|
| 134 |
+
|
| 135 |
+
- **Use clear, concise language**
|
| 136 |
+
- **Provide examples** for complex features
|
| 137 |
+
- **Include screenshots** for UI changes
|
| 138 |
+
- **Update both English content and code comments**
|
| 139 |
+
|
| 140 |
+
### Documentation Structure
|
| 141 |
+
|
| 142 |
+
- **README.md**: Project overview and quick start
|
| 143 |
+
- **CONTRIBUTING.md**: This file
|
| 144 |
+
- **Content files**: In `src/content/chapters/demo/`
|
| 145 |
+
- **Component docs**: Inline comments and examples
|
| 146 |
+
|
| 147 |
+
## 🎯 Areas for Contribution
|
| 148 |
+
|
| 149 |
+
### High Priority
|
| 150 |
+
|
| 151 |
+
- **Bug fixes** and stability improvements
|
| 152 |
+
- **Accessibility enhancements**
|
| 153 |
+
- **Mobile responsiveness**
|
| 154 |
+
- **Performance optimizations**
|
| 155 |
+
- **Documentation improvements**
|
| 156 |
+
|
| 157 |
+
### Feature Ideas
|
| 158 |
+
|
| 159 |
+
- **New interactive components**
|
| 160 |
+
- **Additional export formats**
|
| 161 |
+
- **Enhanced LaTeX import**
|
| 162 |
+
- **Theme customization**
|
| 163 |
+
- **Plugin system**
|
| 164 |
+
|
| 165 |
+
### Community
|
| 166 |
+
|
| 167 |
+
- **Answer questions** in discussions
|
| 168 |
+
- **Share examples** of your work
|
| 169 |
+
- **Write tutorials** and guides
|
| 170 |
+
- **Help with translations**
|
| 171 |
+
|
| 172 |
+
## 🚫 What Not to Contribute
|
| 173 |
+
|
| 174 |
+
- **Breaking changes** without discussion
|
| 175 |
+
- **Major architectural changes** without approval
|
| 176 |
+
- **Dependencies** that significantly increase bundle size
|
| 177 |
+
- **Features** that don't align with the project's goals
|
| 178 |
+
|
| 179 |
+
## 📞 Getting Help
|
| 180 |
+
|
| 181 |
+
- **Discussions**: [Community tab](https://huggingface.co/spaces/tfrere/research-article-template/discussions)
|
| 182 |
+
- **Issues**: [Report bugs](https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=issue)
|
| 183 |
+
- **Contact**: [@tfrere](https://huggingface.co/tfrere) on Hugging Face
|
| 184 |
+
|
| 185 |
+
## 📄 License
|
| 186 |
+
|
| 187 |
+
By contributing, you agree that your contributions will be licensed under the same [CC-BY-4.0 license](LICENSE) that covers the project.
|
| 188 |
+
|
| 189 |
+
## 🙏 Recognition
|
| 190 |
+
|
| 191 |
+
Contributors will be:
|
| 192 |
+
- **Listed in acknowledgments** (if desired)
|
| 193 |
+
- **Mentioned in release notes** for significant contributions
|
| 194 |
+
- **Credited** in relevant documentation
|
| 195 |
+
|
| 196 |
+
Thank you for helping make scientific writing more accessible and interactive! 🎉
|
Dockerfile
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Use an official Node runtime as the base image for building the application
|
| 2 |
# Build with Playwright (browsers and deps ready)
|
| 3 |
-
FROM mcr.microsoft.com/playwright:v1.
|
| 4 |
|
| 5 |
# Install git, git-lfs, and dependencies for Pandoc (only if ENABLE_LATEX_CONVERSION=true)
|
| 6 |
RUN apt-get update && apt-get install -y git git-lfs wget && apt-get clean
|
|
@@ -24,7 +24,7 @@ RUN npm install
|
|
| 24 |
COPY app/ .
|
| 25 |
|
| 26 |
# Conditionally convert LaTeX to MDX if ENABLE_LATEX_CONVERSION=true
|
| 27 |
-
ARG ENABLE_LATEX_CONVERSION=
|
| 28 |
RUN if [ "$ENABLE_LATEX_CONVERSION" = "true" ]; then \
|
| 29 |
echo "🔄 LaTeX importer enabled - running latex:convert..."; \
|
| 30 |
npm run latex:convert; \
|
|
@@ -32,10 +32,6 @@ RUN if [ "$ENABLE_LATEX_CONVERSION" = "true" ]; then \
|
|
| 32 |
echo "⏭️ LaTeX importer disabled - skipping..."; \
|
| 33 |
fi
|
| 34 |
|
| 35 |
-
# Pre-install notion-importer dependencies (for runtime import)
|
| 36 |
-
# Note: Notion import is done at RUNTIME (not build time) to access secrets
|
| 37 |
-
RUN cd scripts/notion-importer && npm install && cd ../..
|
| 38 |
-
|
| 39 |
# Ensure `public/data` is a real directory with real files (not a symlink)
|
| 40 |
# This handles the case where `public/data` is a symlink in the repo, which
|
| 41 |
# would be broken inside the container after COPY.
|
|
@@ -46,32 +42,30 @@ RUN set -e; \
|
|
| 46 |
mkdir -p public/data; \
|
| 47 |
cp -a src/content/assets/data/. public/data/
|
| 48 |
|
| 49 |
-
# Build the application
|
| 50 |
RUN npm run build
|
| 51 |
|
| 52 |
# Generate the PDF (light theme, full wait)
|
| 53 |
RUN npm run export:pdf -- --theme=light --wait=full
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
|
| 61 |
-
# Copy
|
| 62 |
COPY nginx.conf /etc/nginx/nginx.conf
|
| 63 |
|
| 64 |
-
# Copy entrypoint script
|
| 65 |
-
COPY entrypoint.sh /entrypoint.sh
|
| 66 |
-
RUN chmod +x /entrypoint.sh
|
| 67 |
-
|
| 68 |
# Create necessary directories and set permissions
|
| 69 |
-
RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx
|
| 70 |
-
chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
|
| 73 |
# Expose port 8080
|
| 74 |
EXPOSE 8080
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
|
|
|
|
| 1 |
# Use an official Node runtime as the base image for building the application
|
| 2 |
# Build with Playwright (browsers and deps ready)
|
| 3 |
+
FROM mcr.microsoft.com/playwright:v1.55.0-jammy AS build
|
| 4 |
|
| 5 |
# Install git, git-lfs, and dependencies for Pandoc (only if ENABLE_LATEX_CONVERSION=true)
|
| 6 |
RUN apt-get update && apt-get install -y git git-lfs wget && apt-get clean
|
|
|
|
| 24 |
COPY app/ .
|
| 25 |
|
| 26 |
# Conditionally convert LaTeX to MDX if ENABLE_LATEX_CONVERSION=true
|
| 27 |
+
ARG ENABLE_LATEX_CONVERSION=true
|
| 28 |
RUN if [ "$ENABLE_LATEX_CONVERSION" = "true" ]; then \
|
| 29 |
echo "🔄 LaTeX importer enabled - running latex:convert..."; \
|
| 30 |
npm run latex:convert; \
|
|
|
|
| 32 |
echo "⏭️ LaTeX importer disabled - skipping..."; \
|
| 33 |
fi
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
# Ensure `public/data` is a real directory with real files (not a symlink)
|
| 36 |
# This handles the case where `public/data` is a symlink in the repo, which
|
| 37 |
# would be broken inside the container after COPY.
|
|
|
|
| 42 |
mkdir -p public/data; \
|
| 43 |
cp -a src/content/assets/data/. public/data/
|
| 44 |
|
| 45 |
+
# Build the application
|
| 46 |
RUN npm run build
|
| 47 |
|
| 48 |
# Generate the PDF (light theme, full wait)
|
| 49 |
RUN npm run export:pdf -- --theme=light --wait=full
|
| 50 |
|
| 51 |
+
# Use an official Nginx runtime as the base image for serving the application
|
| 52 |
+
FROM nginx:alpine
|
| 53 |
|
| 54 |
+
# Copy the built application from the build stage
|
| 55 |
+
COPY --from=build /app/dist /usr/share/nginx/html
|
| 56 |
|
| 57 |
+
# Copy a custom Nginx configuration file
|
| 58 |
COPY nginx.conf /etc/nginx/nginx.conf
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# Create necessary directories and set permissions
|
| 61 |
+
RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx && \
|
| 62 |
+
chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /etc/nginx/nginx.conf
|
| 63 |
+
|
| 64 |
+
# Switch to non-root user
|
| 65 |
+
USER nginx
|
| 66 |
|
| 67 |
# Expose port 8080
|
| 68 |
EXPOSE 8080
|
| 69 |
|
| 70 |
+
# Command to run the application
|
| 71 |
+
CMD ["nginx", "-g", "daemon off;"]
|
LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Creative Commons Attribution 4.0 International License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Thibaud Frere
|
| 4 |
+
|
| 5 |
+
This work is licensed under the Creative Commons Attribution 4.0 International License.
|
| 6 |
+
To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/
|
| 7 |
+
or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
|
| 8 |
+
|
| 9 |
+
You are free to:
|
| 10 |
+
|
| 11 |
+
Share — copy and redistribute the material in any medium or format
|
| 12 |
+
Adapt — remix, transform, and build upon the material for any purpose, even commercially.
|
| 13 |
+
|
| 14 |
+
The licensor cannot revoke these freedoms as long as you follow the license terms.
|
| 15 |
+
|
| 16 |
+
Under the following terms:
|
| 17 |
+
|
| 18 |
+
Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
| 19 |
+
|
| 20 |
+
No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
|
| 21 |
+
|
| 22 |
+
Notices:
|
| 23 |
+
|
| 24 |
+
You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
|
| 25 |
+
|
| 26 |
+
No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
For the source code and technical implementation:
|
| 31 |
+
- The source code is available at: https://huggingface.co/spaces/tfrere/research-article-template
|
| 32 |
+
- Third-party figures and assets are excluded from this license and marked in their captions
|
| 33 |
+
- Dependencies and third-party libraries maintain their respective licenses
|
app/astro.config.mjs
CHANGED
|
@@ -56,11 +56,13 @@ export default defineConfig({
|
|
| 56 |
rehypePlugins: [
|
| 57 |
rehypeSlug,
|
| 58 |
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
|
| 59 |
-
[rehypeKatex, {
|
|
|
|
|
|
|
| 60 |
[rehypeCitation, {
|
| 61 |
bibliography: 'src/content/bibliography.bib',
|
| 62 |
linkCitations: true,
|
| 63 |
-
csl: "apa"
|
| 64 |
}],
|
| 65 |
rehypeReferencesAndFootnotes,
|
| 66 |
rehypeRestoreAtInCode,
|
|
|
|
| 56 |
rehypePlugins: [
|
| 57 |
rehypeSlug,
|
| 58 |
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
|
| 59 |
+
[rehypeKatex, {
|
| 60 |
+
trust: true,
|
| 61 |
+
}],
|
| 62 |
[rehypeCitation, {
|
| 63 |
bibliography: 'src/content/bibliography.bib',
|
| 64 |
linkCitations: true,
|
| 65 |
+
csl: "apa",
|
| 66 |
}],
|
| 67 |
rehypeReferencesAndFootnotes,
|
| 68 |
rehypeRestoreAtInCode,
|
app/package.json
CHANGED
|
Binary files a/app/package.json and b/app/package.json differ
|
|
|
app/public/scripts/color-palettes.js
CHANGED
|
@@ -46,63 +46,95 @@
|
|
| 46 |
return { r, g, b: b3 };
|
| 47 |
};
|
| 48 |
const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
|
| 49 |
-
const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a*a + b*b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
|
| 50 |
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
| 51 |
const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
|
| 52 |
-
const toHex = ({ r, g, b }) => {
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
|
|
|
| 57 |
const css = getCssVar('--primary-color');
|
| 58 |
-
if (!css) return
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
const rgb = parseCssColorToRgb(css);
|
| 61 |
-
if (rgb)
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
};
|
| 64 |
// No count management via CSS anymore; counts are passed directly to the API
|
| 65 |
|
| 66 |
const generators = {
|
| 67 |
-
categorical: (
|
| 68 |
-
const
|
| 69 |
-
const { r, g, b } = parseHex(baseHex);
|
| 70 |
-
const { L, a, b: bb } = rgbToOklab(r,g,b);
|
| 71 |
-
const { C, h } = oklabToOklch(L,a,bb);
|
| 72 |
const L0 = Math.min(0.85, Math.max(0.4, L));
|
| 73 |
const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
|
| 74 |
const total = Math.max(1, Math.min(12, count || 8));
|
| 75 |
const hueStep = 360 / total;
|
| 76 |
const results = [];
|
| 77 |
-
for (let i=0;i<total;i++) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
return results;
|
| 79 |
},
|
| 80 |
-
sequential: (
|
| 81 |
-
const
|
| 82 |
-
const { r, g, b } = parseHex(baseHex);
|
| 83 |
-
const { L, a, b: bb } = rgbToOklab(r,g,b);
|
| 84 |
-
const { C, h } = oklabToOklch(L,a,bb);
|
| 85 |
const total = Math.max(1, Math.min(12, count || 8));
|
| 86 |
const startL = Math.max(0.25, L - 0.18);
|
| 87 |
const endL = Math.min(0.92, L + 0.18);
|
| 88 |
const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
|
| 89 |
const out = [];
|
| 90 |
-
for (let i=0;i<total;i++) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
return out;
|
| 92 |
},
|
| 93 |
-
diverging: (
|
| 94 |
-
const
|
| 95 |
-
const { r, g, b } = parseHex(baseHex);
|
| 96 |
-
const baseLab = rgbToOklab(r,g,b);
|
| 97 |
-
const baseLch = oklabToOklch(baseLab.L, baseLab.a, baseLab.b);
|
| 98 |
const total = Math.max(1, Math.min(12, count || 8));
|
| 99 |
|
| 100 |
// Left endpoint: EXACT primary color (no darkening)
|
| 101 |
-
const leftLab =
|
| 102 |
// Right endpoint: complement with same L and similar C (clamped safe)
|
| 103 |
-
const compH = (
|
| 104 |
-
const cSafe = Math.min(0.35, Math.max(0.08,
|
| 105 |
-
const rightLab = oklchToOklab(
|
| 106 |
const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
|
| 107 |
|
| 108 |
const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
|
|
@@ -152,18 +184,22 @@
|
|
| 152 |
let lastSignature = '';
|
| 153 |
|
| 154 |
const updatePalettes = () => {
|
| 155 |
-
const
|
| 156 |
-
const
|
|
|
|
| 157 |
if (signature === lastSignature) return;
|
| 158 |
lastSignature = signature;
|
| 159 |
-
try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {}
|
| 160 |
};
|
| 161 |
|
| 162 |
const bootstrap = () => {
|
|
|
|
| 163 |
updatePalettes();
|
|
|
|
|
|
|
| 164 |
const mo = new MutationObserver(() => updatePalettes());
|
| 165 |
mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
|
| 166 |
-
|
| 167 |
// Utility: choose high-contrast (or softened) text style against an arbitrary background color
|
| 168 |
const pickTextStyleForBackground = (bgCss, opts = {}) => {
|
| 169 |
const cssRoot = document.documentElement;
|
|
@@ -175,13 +211,13 @@
|
|
| 175 |
if (!rgb) return null;
|
| 176 |
return rgb; // already 0..1
|
| 177 |
};
|
| 178 |
-
const mixRgb01 = (a, b, t) => ({ r: a.r*(1-t)+b.r*t, g: a.g*(1-t)+b.g*t, b: a.b*(1-t)+b.b*t });
|
| 179 |
const relLum = (rgb) => {
|
| 180 |
const f = (u) => srgbToLinear(u);
|
| 181 |
-
return 0.2126*f(rgb.r) + 0.7152*f(rgb.g) + 0.0722*f(rgb.b);
|
| 182 |
};
|
| 183 |
const contrast = (fg, bg) => {
|
| 184 |
-
const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1,L2), b = Math.min(L1,L2);
|
| 185 |
return (a + 0.05) / (b + 0.05);
|
| 186 |
};
|
| 187 |
try {
|
|
@@ -193,7 +229,7 @@
|
|
| 193 |
.filter(x => !!x.rgb);
|
| 194 |
// Pick the max contrast
|
| 195 |
let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
|
| 196 |
-
for (let i=1;i<candidates.length;i++){
|
| 197 |
const cr = contrast(candidates[i].rgb, bg);
|
| 198 |
if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
|
| 199 |
}
|
|
@@ -206,7 +242,7 @@
|
|
| 206 |
finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
|
| 207 |
}
|
| 208 |
const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
|
| 209 |
-
const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40*haloStrength})` : `rgba(0,0,0,${0.30 + 0.30*haloStrength})`;
|
| 210 |
return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
|
| 211 |
} catch {
|
| 212 |
return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
|
|
@@ -214,14 +250,16 @@
|
|
| 214 |
};
|
| 215 |
window.ColorPalettes = {
|
| 216 |
refresh: updatePalettes,
|
| 217 |
-
notify: () => { try { const
|
| 218 |
getPrimary: () => getPrimaryHex(),
|
|
|
|
| 219 |
getColors: (key, count = 6) => {
|
| 220 |
-
const
|
|
|
|
| 221 |
const total = Math.max(1, Math.min(12, Number(count) || 6));
|
| 222 |
-
if (key === 'categorical') return generators.categorical(
|
| 223 |
-
if (key === 'sequential') return generators.sequential(
|
| 224 |
-
if (key === 'diverging') return generators.diverging(
|
| 225 |
return [];
|
| 226 |
},
|
| 227 |
getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
|
|
|
|
| 46 |
return { r, g, b: b3 };
|
| 47 |
};
|
| 48 |
const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
|
| 49 |
+
const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a * a + b * b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
|
| 50 |
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
| 51 |
const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
|
| 52 |
+
const toHex = ({ r, g, b }) => {
|
| 53 |
+
const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255);
|
| 54 |
+
const h = (n) => n.toString(16).padStart(2, '0');
|
| 55 |
+
return `#${h(R)}${h(G)}${h(B)}`.toUpperCase();
|
| 56 |
+
};
|
| 57 |
+
const oklchToHexSafe = (L, C, h) => { let c = C; for (let i = 0; i < 12; i++) { const { a, b } = oklchToOklab(L, c, h); const rgb = oklabToRgb(L, a, b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c - 0.02); } return toHex(oklabToRgb(L, 0, 0)); };
|
| 58 |
+
const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1]) / 255, g: Number(m[2]) / 255, b: Number(m[3]) / 255 }; } catch { return null; } };
|
| 59 |
|
| 60 |
+
// Get primary color in OKLCH format to preserve precision
|
| 61 |
+
const getPrimaryOKLCH = () => {
|
| 62 |
const css = getCssVar('--primary-color');
|
| 63 |
+
if (!css) return null;
|
| 64 |
+
|
| 65 |
+
// For OKLCH colors, return the exact values without conversion
|
| 66 |
+
if (css.includes('oklch')) {
|
| 67 |
+
const oklchMatch = css.match(/oklch\(([^)]+)\)/);
|
| 68 |
+
if (oklchMatch) {
|
| 69 |
+
const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim()));
|
| 70 |
+
if (values.length >= 3) {
|
| 71 |
+
const [L, C, h] = values;
|
| 72 |
+
return { L, C, h };
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// For non-OKLCH colors, convert to OKLCH for consistency
|
| 78 |
const rgb = parseCssColorToRgb(css);
|
| 79 |
+
if (rgb) {
|
| 80 |
+
const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b);
|
| 81 |
+
const { C, h } = oklabToOklch(L, a, b);
|
| 82 |
+
return { L, C, h };
|
| 83 |
+
}
|
| 84 |
+
return null;
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
// Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH
|
| 88 |
+
const getPrimaryHex = () => {
|
| 89 |
+
const oklch = getPrimaryOKLCH();
|
| 90 |
+
if (!oklch) return null;
|
| 91 |
+
|
| 92 |
+
const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h);
|
| 93 |
+
const rgb = oklabToRgb(oklch.L, a, b);
|
| 94 |
+
return toHex(rgb);
|
| 95 |
};
|
| 96 |
// No count management via CSS anymore; counts are passed directly to the API
|
| 97 |
|
| 98 |
const generators = {
|
| 99 |
+
categorical: (baseOKLCH, count) => {
|
| 100 |
+
const { L, C, h } = baseOKLCH;
|
|
|
|
|
|
|
|
|
|
| 101 |
const L0 = Math.min(0.85, Math.max(0.4, L));
|
| 102 |
const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
|
| 103 |
const total = Math.max(1, Math.min(12, count || 8));
|
| 104 |
const hueStep = 360 / total;
|
| 105 |
const results = [];
|
| 106 |
+
for (let i = 0; i < total; i++) {
|
| 107 |
+
const hDeg = (h + i * hueStep) % 360;
|
| 108 |
+
const lVar = ((i % 3) - 1) * 0.04;
|
| 109 |
+
results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg));
|
| 110 |
+
}
|
| 111 |
return results;
|
| 112 |
},
|
| 113 |
+
sequential: (baseOKLCH, count) => {
|
| 114 |
+
const { L, C, h } = baseOKLCH;
|
|
|
|
|
|
|
|
|
|
| 115 |
const total = Math.max(1, Math.min(12, count || 8));
|
| 116 |
const startL = Math.max(0.25, L - 0.18);
|
| 117 |
const endL = Math.min(0.92, L + 0.18);
|
| 118 |
const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
|
| 119 |
const out = [];
|
| 120 |
+
for (let i = 0; i < total; i++) {
|
| 121 |
+
const t = total === 1 ? 0 : i / (total - 1);
|
| 122 |
+
const lNow = startL * (1 - t) + endL * t;
|
| 123 |
+
const cNow = cBase * (0.85 + 0.15 * (1 - Math.abs(0.5 - t) * 2));
|
| 124 |
+
out.push(oklchToHexSafe(lNow, cNow, h));
|
| 125 |
+
}
|
| 126 |
return out;
|
| 127 |
},
|
| 128 |
+
diverging: (baseOKLCH, count) => {
|
| 129 |
+
const { L, C, h } = baseOKLCH;
|
|
|
|
|
|
|
|
|
|
| 130 |
const total = Math.max(1, Math.min(12, count || 8));
|
| 131 |
|
| 132 |
// Left endpoint: EXACT primary color (no darkening)
|
| 133 |
+
const leftLab = oklchToOklab(L, C, h);
|
| 134 |
// Right endpoint: complement with same L and similar C (clamped safe)
|
| 135 |
+
const compH = (h + 180) % 360;
|
| 136 |
+
const cSafe = Math.min(0.35, Math.max(0.08, C));
|
| 137 |
+
const rightLab = oklchToOklab(L, cSafe, compH);
|
| 138 |
const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
|
| 139 |
|
| 140 |
const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
|
|
|
|
| 184 |
let lastSignature = '';
|
| 185 |
|
| 186 |
const updatePalettes = () => {
|
| 187 |
+
const primaryOKLCH = getPrimaryOKLCH();
|
| 188 |
+
const primaryHex = getPrimaryHex();
|
| 189 |
+
const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`;
|
| 190 |
if (signature === lastSignature) return;
|
| 191 |
lastSignature = signature;
|
| 192 |
+
try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { }
|
| 193 |
};
|
| 194 |
|
| 195 |
const bootstrap = () => {
|
| 196 |
+
// Initial setup - only run once on page load
|
| 197 |
updatePalettes();
|
| 198 |
+
|
| 199 |
+
// Observer will handle all subsequent changes
|
| 200 |
const mo = new MutationObserver(() => updatePalettes());
|
| 201 |
mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
|
| 202 |
+
|
| 203 |
// Utility: choose high-contrast (or softened) text style against an arbitrary background color
|
| 204 |
const pickTextStyleForBackground = (bgCss, opts = {}) => {
|
| 205 |
const cssRoot = document.documentElement;
|
|
|
|
| 211 |
if (!rgb) return null;
|
| 212 |
return rgb; // already 0..1
|
| 213 |
};
|
| 214 |
+
const mixRgb01 = (a, b, t) => ({ r: a.r * (1 - t) + b.r * t, g: a.g * (1 - t) + b.g * t, b: a.b * (1 - t) + b.b * t });
|
| 215 |
const relLum = (rgb) => {
|
| 216 |
const f = (u) => srgbToLinear(u);
|
| 217 |
+
return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b);
|
| 218 |
};
|
| 219 |
const contrast = (fg, bg) => {
|
| 220 |
+
const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2);
|
| 221 |
return (a + 0.05) / (b + 0.05);
|
| 222 |
};
|
| 223 |
try {
|
|
|
|
| 229 |
.filter(x => !!x.rgb);
|
| 230 |
// Pick the max contrast
|
| 231 |
let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
|
| 232 |
+
for (let i = 1; i < candidates.length; i++) {
|
| 233 |
const cr = contrast(candidates[i].rgb, bg);
|
| 234 |
if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
|
| 235 |
}
|
|
|
|
| 242 |
finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
|
| 243 |
}
|
| 244 |
const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
|
| 245 |
+
const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40 * haloStrength})` : `rgba(0,0,0,${0.30 + 0.30 * haloStrength})`;
|
| 246 |
return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
|
| 247 |
} catch {
|
| 248 |
return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
|
|
|
|
| 250 |
};
|
| 251 |
window.ColorPalettes = {
|
| 252 |
refresh: updatePalettes,
|
| 253 |
+
notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } },
|
| 254 |
getPrimary: () => getPrimaryHex(),
|
| 255 |
+
getPrimaryOKLCH: () => getPrimaryOKLCH(),
|
| 256 |
getColors: (key, count = 6) => {
|
| 257 |
+
const primaryOKLCH = getPrimaryOKLCH();
|
| 258 |
+
if (!primaryOKLCH) return [];
|
| 259 |
const total = Math.max(1, Math.min(12, Number(count) || 6));
|
| 260 |
+
if (key === 'categorical') return generators.categorical(primaryOKLCH, total);
|
| 261 |
+
if (key === 'sequential') return generators.sequential(primaryOKLCH, total);
|
| 262 |
+
if (key === 'diverging') return generators.diverging(primaryOKLCH, total);
|
| 263 |
return [];
|
| 264 |
},
|
| 265 |
getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
|
app/scripts/notion-importer/.notion-to-md/media/27877f1c-9c9d-804d-9c82-f7b3905578ff_media.json
CHANGED
|
@@ -1,198 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
"mediaEntries": {
|
| 5 |
-
"27877f1c-9c9d-8078-b6da-c7a4c67c8f35": {
|
| 6 |
-
"mediaInfo": {
|
| 7 |
-
"type": "DOWNLOAD",
|
| 8 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/69940e63-ba02-4d6b-bc79-81dcab16590b/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=0e7a6d2cf89a76128b1e4ecd8e12053deb0a5e802a15e3e8c3d7979e2131fa09&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 9 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8078-b6da-c7a4c67c8f35.png",
|
| 10 |
-
"sourceType": "block",
|
| 11 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8078-b6da-c7a4c67c8f35.png"
|
| 12 |
-
},
|
| 13 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 14 |
-
"createdAt": "2025-09-24T09:36:35.892Z",
|
| 15 |
-
"updatedAt": "2025-10-08T13:00:25.689Z"
|
| 16 |
-
},
|
| 17 |
-
"27877f1c-9c9d-8014-834f-d700b623256b": {
|
| 18 |
-
"mediaInfo": {
|
| 19 |
-
"type": "DOWNLOAD",
|
| 20 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/a11fbd23-10f8-4485-ad35-f3de9d480449/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=9145e81d85d35d531ffb6bbabad25d5846aa559613d371bc2880f6d02529b799&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 21 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8014-834f-d700b623256b.png",
|
| 22 |
-
"sourceType": "block",
|
| 23 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8014-834f-d700b623256b.png"
|
| 24 |
-
},
|
| 25 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 26 |
-
"createdAt": "2025-09-24T09:36:35.899Z",
|
| 27 |
-
"updatedAt": "2025-10-08T13:00:25.703Z"
|
| 28 |
-
},
|
| 29 |
-
"27877f1c-9c9d-808d-9c6d-fae817ac8868": {
|
| 30 |
-
"mediaInfo": {
|
| 31 |
-
"type": "DOWNLOAD",
|
| 32 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/9b549e26-b527-4ad5-b1ad-3ed049747090/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=b9e650906f6978d3d01caebf5329c1571bfa97d53d88a3c571eaaa7b618970d2&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 33 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808d-9c6d-fae817ac8868.png",
|
| 34 |
-
"sourceType": "block",
|
| 35 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808d-9c6d-fae817ac8868.png"
|
| 36 |
-
},
|
| 37 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 38 |
-
"createdAt": "2025-09-24T09:36:35.943Z",
|
| 39 |
-
"updatedAt": "2025-10-08T13:00:25.727Z"
|
| 40 |
-
},
|
| 41 |
-
"27877f1c-9c9d-8075-ae2e-dc24fe9296ca": {
|
| 42 |
-
"mediaInfo": {
|
| 43 |
-
"type": "DOWNLOAD",
|
| 44 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/943ada15-c18b-4434-ac96-cca2440119db/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=f6c6aeafa758351188cf92575d7adbac6d4438d61891b377861cceebe4699c16&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 45 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8075-ae2e-dc24fe9296ca.png",
|
| 46 |
-
"sourceType": "block",
|
| 47 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8075-ae2e-dc24fe9296ca.png"
|
| 48 |
-
},
|
| 49 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 50 |
-
"createdAt": "2025-09-24T09:36:36.095Z",
|
| 51 |
-
"updatedAt": "2025-10-08T13:00:25.926Z"
|
| 52 |
-
},
|
| 53 |
-
"27877f1c-9c9d-801d-841a-e35011491566": {
|
| 54 |
-
"mediaInfo": {
|
| 55 |
-
"type": "DOWNLOAD",
|
| 56 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/3a912fb2-01eb-46d2-9687-41ffbb5b0446/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=fdbb11308e72b72824529e50a6386bf06785d0576e5c27d6cf6f609380614733&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 57 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-801d-841a-e35011491566.png",
|
| 58 |
-
"sourceType": "block",
|
| 59 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-801d-841a-e35011491566.png"
|
| 60 |
-
},
|
| 61 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 62 |
-
"createdAt": "2025-09-24T09:36:36.121Z",
|
| 63 |
-
"updatedAt": "2025-10-08T13:00:25.898Z"
|
| 64 |
-
},
|
| 65 |
-
"27877f1c-9c9d-8048-9b7e-db4fa7485915": {
|
| 66 |
-
"mediaInfo": {
|
| 67 |
-
"type": "DOWNLOAD",
|
| 68 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/58651851-13c8-4e99-92e3-857c8e3d305b/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=f13847b038a80822deb40d3e0873722dc40cb227be4a6edfdb70d27be0794729&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 69 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8048-9b7e-db4fa7485915.png",
|
| 70 |
-
"sourceType": "block",
|
| 71 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8048-9b7e-db4fa7485915.png"
|
| 72 |
-
},
|
| 73 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 74 |
-
"createdAt": "2025-09-24T09:36:36.136Z",
|
| 75 |
-
"updatedAt": "2025-10-08T13:00:25.917Z"
|
| 76 |
-
},
|
| 77 |
-
"27877f1c-9c9d-804d-bd0a-e0b1c15e504f": {
|
| 78 |
-
"mediaInfo": {
|
| 79 |
-
"type": "DOWNLOAD",
|
| 80 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/3fbc7e6c-8ec0-4ef5-9161-7aef75930323/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130014Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=fd7ef992dc4f3a290a5f04a0a640fe2828b5a33487682c4956913fe674c9c017&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 81 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-804d-bd0a-e0b1c15e504f.png",
|
| 82 |
-
"sourceType": "block",
|
| 83 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-804d-bd0a-e0b1c15e504f.png"
|
| 84 |
-
},
|
| 85 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 86 |
-
"createdAt": "2025-09-24T09:36:36.244Z",
|
| 87 |
-
"updatedAt": "2025-10-08T13:00:26.009Z"
|
| 88 |
-
},
|
| 89 |
-
"27877f1c-9c9d-80b9-8cfb-f0a6aaaa8760": {
|
| 90 |
-
"mediaInfo": {
|
| 91 |
-
"type": "DOWNLOAD",
|
| 92 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/d3899f55-47ba-45b0-be0f-9abc5bb4f247/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=0e702a4db1c4dabdd69b83b59535958cd39a3e2906332aa607e61864797bca59&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 93 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b9-8cfb-f0a6aaaa8760.png",
|
| 94 |
-
"sourceType": "block",
|
| 95 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b9-8cfb-f0a6aaaa8760.png"
|
| 96 |
-
},
|
| 97 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 98 |
-
"createdAt": "2025-09-24T09:36:36.247Z",
|
| 99 |
-
"updatedAt": "2025-10-08T13:00:26.000Z"
|
| 100 |
-
},
|
| 101 |
-
"27877f1c-9c9d-80aa-b968-c54c9fe7e5d7": {
|
| 102 |
-
"mediaInfo": {
|
| 103 |
-
"type": "DOWNLOAD",
|
| 104 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/1f240ffd-b4d7-422a-9a81-0aabda1f6c16/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=c353bf2bf0d4ef95c66531df5c779035a90add76e2c286e9e26d41c1a6526eb9&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 105 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80aa-b968-c54c9fe7e5d7.png",
|
| 106 |
-
"sourceType": "block",
|
| 107 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80aa-b968-c54c9fe7e5d7.png"
|
| 108 |
-
},
|
| 109 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 110 |
-
"createdAt": "2025-09-24T09:36:36.247Z",
|
| 111 |
-
"updatedAt": "2025-10-08T13:00:26.061Z"
|
| 112 |
-
},
|
| 113 |
-
"27877f1c-9c9d-8013-b668-f14bd1ac0ec0": {
|
| 114 |
-
"mediaInfo": {
|
| 115 |
-
"type": "DOWNLOAD",
|
| 116 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/cb01b334-89a5-436f-a141-b7c310d25b57/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=ff0a0599c70fd9f202d55e4a7e887de8d8727724550d0e9faf6df28e0e9b7696&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 117 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8013-b668-f14bd1ac0ec0.png",
|
| 118 |
-
"sourceType": "block",
|
| 119 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8013-b668-f14bd1ac0ec0.png"
|
| 120 |
-
},
|
| 121 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 122 |
-
"createdAt": "2025-09-24T09:36:36.250Z",
|
| 123 |
-
"updatedAt": "2025-10-08T13:00:26.056Z"
|
| 124 |
-
},
|
| 125 |
-
"27877f1c-9c9d-80e9-b729-dbd328930bed": {
|
| 126 |
-
"mediaInfo": {
|
| 127 |
-
"type": "DOWNLOAD",
|
| 128 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/9d723e06-252f-44e3-844e-1410fa92667a/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=4faf0d241e315ba80e5f56bb4cb410229c15e429fcab990fd1ebc2b17e2e4388&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 129 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e9-b729-dbd328930bed.png",
|
| 130 |
-
"sourceType": "block",
|
| 131 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e9-b729-dbd328930bed.png"
|
| 132 |
-
},
|
| 133 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 134 |
-
"createdAt": "2025-09-24T09:36:36.251Z",
|
| 135 |
-
"updatedAt": "2025-10-08T13:00:26.048Z"
|
| 136 |
-
},
|
| 137 |
-
"27877f1c-9c9d-80a9-b4d0-f2129716632d": {
|
| 138 |
-
"mediaInfo": {
|
| 139 |
-
"type": "DOWNLOAD",
|
| 140 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/137c2c51-6f89-437f-ba45-3a844c747b7a/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466W5FHSQK6%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130018Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJGMEQCIAhuEUuJdQLuRcUIqWrlF%2FxJVRmDyl6u61kVLhtjlfZUAiBV%2Fg6BRYZGv8gjl2KUiGhGUm6oydw9R%2BGbL4YBC8orxyqIBAi9%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIM3uBXzGO9Ig0qNGY4KtwD8SFvKEBtLvytudTAIznpwZ0LlbKJs50UtNCuHXZMaMCWVzh3bh5s1YUDv9F8QzpxpmwoEHNA0zdieOQ0RTayznMP%2FQonTMVpq110QUSpjw0oiOQAObBO%2FZhOW3eVIvyERdtG8hJ%2FFdtOx68HiwShjXQNIx%2FTLP%2FyKsamFu97bNFKLLbXaKEQL%2FPeYBMfUh1FOikwzUsWtnX6xkIyDZlzyvR5XfiR93IkhPKAe3tRogdfHErVfaMhWD4CLC3CotugLqYLChLu2FEgjljFpEHc5u5od5FnVy9AUpcA9X21VVFEM5ZdQ2e3RAB6wIMQvIaYTHHCnFFfouA8nRqcEVzWysN3qwh2ilXM%2FkIfZN7fRIxbBSPLyUIVcJpZNwbzs8d5bs3qvzM9G8FkzDvtYML3JOdEzF4LuxAAJJLAqhTTQ1PhnB4lalWW%2BzGBluxdvxfidxr7w8b74x1A6sr2RRg204TCYmbv8epUSs%2BRfkjbpQb25T59MrJnDDkvR3vwM0uRiYOcx7BIJoJQ9NABbDhUdQivPKSx%2FWfUI4qzy2Qu3ylcwrudYUrkObXxW8EpCrpCr3q7hWCPkKfvLhioYojN04XKEoAr9eJNJKYu8%2BGtS2%2FHsFqf94Soxz4NYa8wsaWZxwY6pgEneyRps7aPn1nFrZvbD3DUmmNVnG7Ot90eZ9AeaSCgE0a8LPo2EBJqYduCTOFx0ZS31WSpymmuKKyjsI94sx8u%2F5Mmvyc%2F6g6F%2FSGga8Z7F5NIPVciGCTsa2iZFLblK4LGKDfvx5RgoyuAatccTNlNYca%2F5w9iQ1JXOu83l39OW1O54Xr5rX96kIiak60h5LxJgIHU3zeBdD4UeGH39LaRoJyMksdX&X-Amz-Signature=3e45baae308e0004d8e62bc03d6e459d6310e19ae4935db52491727bc24231cb&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 141 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80a9-b4d0-f2129716632d.png",
|
| 142 |
-
"sourceType": "block",
|
| 143 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80a9-b4d0-f2129716632d.png"
|
| 144 |
-
},
|
| 145 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 146 |
-
"createdAt": "2025-09-24T09:36:36.257Z",
|
| 147 |
-
"updatedAt": "2025-10-08T13:00:26.048Z"
|
| 148 |
-
},
|
| 149 |
-
"27877f1c-9c9d-80b6-be07-e8646502f82a": {
|
| 150 |
-
"mediaInfo": {
|
| 151 |
-
"type": "DOWNLOAD",
|
| 152 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/843a4edd-435d-45ca-8212-1af31305fb3a/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=6b41be5cc09cf3fae5304c63a3291650e24c6ad9285ba21202c90a186c8be390&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 153 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b6-be07-e8646502f82a.png",
|
| 154 |
-
"sourceType": "block",
|
| 155 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b6-be07-e8646502f82a.png"
|
| 156 |
-
},
|
| 157 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 158 |
-
"createdAt": "2025-09-24T09:36:36.274Z",
|
| 159 |
-
"updatedAt": "2025-10-08T13:00:25.995Z"
|
| 160 |
-
},
|
| 161 |
-
"27877f1c-9c9d-808f-b712-c7c608da3fc6": {
|
| 162 |
-
"mediaInfo": {
|
| 163 |
-
"type": "DOWNLOAD",
|
| 164 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/33327526-41c3-4e9c-b4ee-c7cc3c23c7ca/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=68518895319bebfef33cc5c5786ea73b408d55f058fe1b07a6e057f3167389dc&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 165 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808f-b712-c7c608da3fc6.png",
|
| 166 |
-
"sourceType": "block",
|
| 167 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808f-b712-c7c608da3fc6.png"
|
| 168 |
-
},
|
| 169 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 170 |
-
"createdAt": "2025-09-24T09:36:36.274Z",
|
| 171 |
-
"updatedAt": "2025-10-08T13:00:25.863Z"
|
| 172 |
-
},
|
| 173 |
-
"27877f1c-9c9d-8031-ac8d-c5678af1bdd5": {
|
| 174 |
-
"mediaInfo": {
|
| 175 |
-
"type": "DOWNLOAD",
|
| 176 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/16831579-d887-4894-b36a-d4df8a134653/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=0e07c27075a366f43cf237c80660a72ba43bdbad3486e7f1f9e3a12afdfcc7f0&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 177 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8031-ac8d-c5678af1bdd5.png",
|
| 178 |
-
"sourceType": "block",
|
| 179 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8031-ac8d-c5678af1bdd5.png"
|
| 180 |
-
},
|
| 181 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 182 |
-
"createdAt": "2025-09-24T09:36:36.276Z",
|
| 183 |
-
"updatedAt": "2025-10-08T13:00:25.920Z"
|
| 184 |
-
},
|
| 185 |
-
"27877f1c-9c9d-80e7-a500-fb79cebde7e3": {
|
| 186 |
-
"mediaInfo": {
|
| 187 |
-
"type": "DOWNLOAD",
|
| 188 |
-
"originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/0458ed42-9805-4c8f-9d7e-c675474a626e/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=e43c1f35e9930e144c0b2b6098457be4848a723301fd78f3ff27d94f87ccbebb&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject",
|
| 189 |
-
"localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e7-a500-fb79cebde7e3.png",
|
| 190 |
-
"sourceType": "block",
|
| 191 |
-
"transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e7-a500-fb79cebde7e3.png"
|
| 192 |
-
},
|
| 193 |
-
"lastEdited": "2025-09-24T09:22:00.000Z",
|
| 194 |
-
"createdAt": "2025-09-24T09:36:36.448Z",
|
| 195 |
-
"updatedAt": "2025-10-08T13:00:26.065Z"
|
| 196 |
-
}
|
| 197 |
-
}
|
| 198 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c282e7bcb40ee2caafda422b3614d996d6023dbb4bbe10f96521348ee151aeb0
|
| 3 |
+
size 36969
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/scripts/notion-importer/README.md
CHANGED
|
@@ -4,25 +4,6 @@ Complete Notion to MDX (Markdown + JSX) importer optimized for Astro with advanc
|
|
| 4 |
|
| 5 |
## 🚀 Quick Start
|
| 6 |
|
| 7 |
-
### Method 1: Using NOTION_PAGE_ID (Recommended)
|
| 8 |
-
|
| 9 |
-
```bash
|
| 10 |
-
# Install dependencies
|
| 11 |
-
npm install
|
| 12 |
-
|
| 13 |
-
# Setup environment variables
|
| 14 |
-
cp env.example .env
|
| 15 |
-
# Edit .env with your Notion token and page ID
|
| 16 |
-
|
| 17 |
-
# Complete Notion → MDX conversion (fetches title/slug automatically)
|
| 18 |
-
NOTION_TOKEN=secret_xxx NOTION_PAGE_ID=abc123 node index.mjs
|
| 19 |
-
|
| 20 |
-
# Or use .env file
|
| 21 |
-
node index.mjs
|
| 22 |
-
```
|
| 23 |
-
|
| 24 |
-
### Method 2: Using pages.json (Legacy)
|
| 25 |
-
|
| 26 |
```bash
|
| 27 |
# Install dependencies
|
| 28 |
npm install
|
|
@@ -31,18 +12,7 @@ npm install
|
|
| 31 |
cp env.example .env
|
| 32 |
# Edit .env with your Notion token
|
| 33 |
|
| 34 |
-
#
|
| 35 |
-
# {
|
| 36 |
-
# "pages": [
|
| 37 |
-
# {
|
| 38 |
-
# "id": "your-page-id",
|
| 39 |
-
# "title": "Title",
|
| 40 |
-
# "slug": "slug"
|
| 41 |
-
# }
|
| 42 |
-
# ]
|
| 43 |
-
# }
|
| 44 |
-
|
| 45 |
-
# Complete Notion → MDX conversion
|
| 46 |
node index.mjs
|
| 47 |
|
| 48 |
# For step-by-step debugging
|
|
@@ -73,7 +43,7 @@ notion-importer/
|
|
| 73 |
### 🎯 **Advanced Media Handling**
|
| 74 |
- **Local download**: Automatic download of all Notion media (images, files, PDFs)
|
| 75 |
- **Path transformation**: Smart path conversion for web accessibility
|
| 76 |
-
- **
|
| 77 |
- **Media organization**: Structured media storage by page ID
|
| 78 |
|
| 79 |
### 🧮 **Interactive Components**
|
|
@@ -121,7 +91,7 @@ published: "2024-01-15"
|
|
| 121 |
tableOfContentsAutoCollapse: true
|
| 122 |
---
|
| 123 |
|
| 124 |
-
import
|
| 125 |
import Note from '../components/Note.astro';
|
| 126 |
import gettingStartedImage from './media/getting-started/image1.png';
|
| 127 |
|
|
|
|
| 4 |
|
| 5 |
## 🚀 Quick Start
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
```bash
|
| 8 |
# Install dependencies
|
| 9 |
npm install
|
|
|
|
| 12 |
cp env.example .env
|
| 13 |
# Edit .env with your Notion token
|
| 14 |
|
| 15 |
+
# Complete Notion → MDX conversion with all features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
node index.mjs
|
| 17 |
|
| 18 |
# For step-by-step debugging
|
|
|
|
| 43 |
### 🎯 **Advanced Media Handling**
|
| 44 |
- **Local download**: Automatic download of all Notion media (images, files, PDFs)
|
| 45 |
- **Path transformation**: Smart path conversion for web accessibility
|
| 46 |
+
- **Figure components**: Automatic conversion to Astro `Figure` components with zoom/download
|
| 47 |
- **Media organization**: Structured media storage by page ID
|
| 48 |
|
| 49 |
### 🧮 **Interactive Components**
|
|
|
|
| 91 |
tableOfContentsAutoCollapse: true
|
| 92 |
---
|
| 93 |
|
| 94 |
+
import Figure from '../components/Figure.astro';
|
| 95 |
import Note from '../components/Note.astro';
|
| 96 |
import gettingStartedImage from './media/getting-started/image1.png';
|
| 97 |
|
app/scripts/notion-importer/env.example
CHANGED
|
@@ -1,2 +1,72 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Notion to MDX Toolkit - Environment Variables
|
| 2 |
+
# Copy this file to .env and fill in your actual values
|
| 3 |
+
|
| 4 |
+
# ===========================================
|
| 5 |
+
# NOTION API CONFIGURATION
|
| 6 |
+
# ===========================================
|
| 7 |
+
|
| 8 |
+
# Your Notion Integration Token
|
| 9 |
+
# Get this from: https://www.notion.so/my-integrations
|
| 10 |
+
# Format: secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 11 |
+
NOTION_TOKEN=secret_your_notion_integration_token_here
|
| 12 |
+
|
| 13 |
+
# ===========================================
|
| 14 |
+
# OPTIONAL CONFIGURATION
|
| 15 |
+
# ===========================================
|
| 16 |
+
|
| 17 |
+
# Custom output directory (optional)
|
| 18 |
+
# Default: ./output
|
| 19 |
+
# OUTPUT_DIR=./my-custom-output
|
| 20 |
+
|
| 21 |
+
# Custom input configuration file (optional)
|
| 22 |
+
# Default: ./input/pages.json
|
| 23 |
+
# INPUT_CONFIG=./my-pages.json
|
| 24 |
+
|
| 25 |
+
# ===========================================
|
| 26 |
+
# USAGE EXAMPLES
|
| 27 |
+
# ===========================================
|
| 28 |
+
|
| 29 |
+
# 1. Basic usage:
|
| 30 |
+
# NOTION_TOKEN=secret_xxx node index.mjs
|
| 31 |
+
|
| 32 |
+
# 2. With custom paths:
|
| 33 |
+
# NOTION_TOKEN=secret_xxx OUTPUT_DIR=./converted node index.mjs
|
| 34 |
+
|
| 35 |
+
# 3. Test access to a page:
|
| 36 |
+
# NOTION_TOKEN=secret_xxx node test-access.mjs
|
| 37 |
+
|
| 38 |
+
# ===========================================
|
| 39 |
+
# SETUP INSTRUCTIONS
|
| 40 |
+
# ===========================================
|
| 41 |
+
|
| 42 |
+
# 1. Create a Notion integration:
|
| 43 |
+
# - Go to https://www.notion.so/my-integrations
|
| 44 |
+
# - Click "New integration"
|
| 45 |
+
# - Give it a name (e.g., "MDX Converter")
|
| 46 |
+
# - Select your workspace
|
| 47 |
+
# - Click "Submit"
|
| 48 |
+
# - Copy the "Internal Integration Token"
|
| 49 |
+
|
| 50 |
+
# 2. Share your Notion pages with the integration:
|
| 51 |
+
# - Open your Notion page
|
| 52 |
+
# - Click "Share" (top right)
|
| 53 |
+
# - Click "Invite"
|
| 54 |
+
# - Search for your integration name
|
| 55 |
+
# - Select it and give "Can read content" permission
|
| 56 |
+
# - Click "Invite"
|
| 57 |
+
|
| 58 |
+
# 3. Configure your pages in input/pages.json:
|
| 59 |
+
# {
|
| 60 |
+
# "pages": [
|
| 61 |
+
# {
|
| 62 |
+
# "id": "your-notion-page-id",
|
| 63 |
+
# "title": "Page Title",
|
| 64 |
+
# "slug": "page-slug"
|
| 65 |
+
# }
|
| 66 |
+
# ]
|
| 67 |
+
# }
|
| 68 |
+
|
| 69 |
+
# 4. Run the conversion:
|
| 70 |
+
# cp env.example .env
|
| 71 |
+
# # Edit .env with your actual token
|
| 72 |
+
# node index.mjs --clean
|
app/scripts/notion-importer/index.mjs
CHANGED
|
@@ -6,10 +6,9 @@ import { fileURLToPath } from 'url';
|
|
| 6 |
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
| 7 |
import { convertNotionToMarkdown } from './notion-converter.mjs';
|
| 8 |
import { convertToMdx } from './mdx-converter.mjs';
|
| 9 |
-
import { Client } from '@notionhq/client';
|
| 10 |
|
| 11 |
-
// Load environment variables from .env file
|
| 12 |
-
config(
|
| 13 |
|
| 14 |
const __filename = fileURLToPath(import.meta.url);
|
| 15 |
const __dirname = dirname(__filename);
|
|
@@ -29,8 +28,7 @@ function parseArgs() {
|
|
| 29 |
clean: false,
|
| 30 |
notionOnly: false,
|
| 31 |
mdxOnly: false,
|
| 32 |
-
token: process.env.NOTION_TOKEN
|
| 33 |
-
pageId: process.env.NOTION_PAGE_ID
|
| 34 |
};
|
| 35 |
|
| 36 |
for (const arg of args) {
|
|
@@ -40,8 +38,6 @@ function parseArgs() {
|
|
| 40 |
config.output = arg.split('=')[1];
|
| 41 |
} else if (arg.startsWith('--token=')) {
|
| 42 |
config.token = arg.split('=')[1];
|
| 43 |
-
} else if (arg.startsWith('--page-id=')) {
|
| 44 |
-
config.pageId = arg.split('=')[1];
|
| 45 |
} else if (arg === '--clean') {
|
| 46 |
config.clean = true;
|
| 47 |
} else if (arg === '--notion-only') {
|
|
@@ -127,54 +123,6 @@ function readPagesConfig(inputFile) {
|
|
| 127 |
}
|
| 128 |
}
|
| 129 |
|
| 130 |
-
/**
|
| 131 |
-
* Create a temporary pages.json from NOTION_PAGE_ID environment variable
|
| 132 |
-
* Extracts title and generates slug from the Notion page
|
| 133 |
-
*/
|
| 134 |
-
async function createPagesConfigFromEnv(pageId, token, outputPath) {
|
| 135 |
-
try {
|
| 136 |
-
console.log('🔍 Fetching page info from Notion API...');
|
| 137 |
-
const notion = new Client({ auth: token });
|
| 138 |
-
const page = await notion.pages.retrieve({ page_id: pageId });
|
| 139 |
-
|
| 140 |
-
// Extract title
|
| 141 |
-
let title = 'Article';
|
| 142 |
-
if (page.properties.title && page.properties.title.title && page.properties.title.title.length > 0) {
|
| 143 |
-
title = page.properties.title.title[0].plain_text;
|
| 144 |
-
} else if (page.properties.Name && page.properties.Name.title && page.properties.Name.title.length > 0) {
|
| 145 |
-
title = page.properties.Name.title[0].plain_text;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
// Generate slug from title
|
| 149 |
-
const slug = title
|
| 150 |
-
.toLowerCase()
|
| 151 |
-
.replace(/[^\w\s-]/g, '')
|
| 152 |
-
.replace(/\s+/g, '-')
|
| 153 |
-
.replace(/-+/g, '-')
|
| 154 |
-
.trim();
|
| 155 |
-
|
| 156 |
-
console.log(` ✅ Found page: "${title}" (slug: ${slug})`);
|
| 157 |
-
|
| 158 |
-
// Create pages config
|
| 159 |
-
const pagesConfig = {
|
| 160 |
-
pages: [{
|
| 161 |
-
id: pageId,
|
| 162 |
-
title: title,
|
| 163 |
-
slug: slug
|
| 164 |
-
}]
|
| 165 |
-
};
|
| 166 |
-
|
| 167 |
-
// Write to temporary file
|
| 168 |
-
writeFileSync(outputPath, JSON.stringify(pagesConfig, null, 4));
|
| 169 |
-
console.log(` ✅ Created temporary pages config`);
|
| 170 |
-
|
| 171 |
-
return pagesConfig;
|
| 172 |
-
} catch (error) {
|
| 173 |
-
console.error(`❌ Error fetching page from Notion: ${error.message}`);
|
| 174 |
-
throw error;
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
function copyToAstroContent(outputDir) {
|
| 179 |
console.log('📋 Copying MDX files to Astro content directory...');
|
| 180 |
|
|
@@ -188,9 +136,7 @@ function copyToAstroContent(outputDir) {
|
|
| 188 |
const mdxFiles = files.filter(file => file.endsWith('.mdx'));
|
| 189 |
if (mdxFiles.length > 0) {
|
| 190 |
const mdxFile = join(outputDir, mdxFiles[0]); // Take the first MDX file
|
| 191 |
-
|
| 192 |
-
const mdxContent = readFileSync(mdxFile, 'utf8');
|
| 193 |
-
writeFileSync(ASTRO_CONTENT_PATH, mdxContent);
|
| 194 |
console.log(` ✅ Copied MDX to ${ASTRO_CONTENT_PATH}`);
|
| 195 |
}
|
| 196 |
|
|
@@ -253,24 +199,6 @@ async function main() {
|
|
| 253 |
console.log('========================');
|
| 254 |
|
| 255 |
try {
|
| 256 |
-
// Prepare input config file
|
| 257 |
-
let inputConfigFile = config.input;
|
| 258 |
-
let pageIdFromEnv = null;
|
| 259 |
-
|
| 260 |
-
// If NOTION_PAGE_ID is provided via env var, create temporary pages.json
|
| 261 |
-
if (config.pageId && config.token) {
|
| 262 |
-
console.log('✨ Using NOTION_PAGE_ID from environment variable');
|
| 263 |
-
const tempConfigPath = join(config.output, '.temp-pages.json');
|
| 264 |
-
ensureDirectory(config.output);
|
| 265 |
-
await createPagesConfigFromEnv(config.pageId, config.token, tempConfigPath);
|
| 266 |
-
inputConfigFile = tempConfigPath;
|
| 267 |
-
pageIdFromEnv = config.pageId;
|
| 268 |
-
} else if (!existsSync(config.input)) {
|
| 269 |
-
console.error(`❌ No NOTION_PAGE_ID environment variable and no pages.json found at: ${config.input}`);
|
| 270 |
-
console.log('💡 Either set NOTION_PAGE_ID env var or create input/pages.json');
|
| 271 |
-
process.exit(1);
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
if (config.clean) {
|
| 275 |
console.log('🧹 Cleaning output directory...');
|
| 276 |
await cleanDirectory(config.output);
|
|
@@ -285,7 +213,7 @@ async function main() {
|
|
| 285 |
} else if (config.notionOnly) {
|
| 286 |
// Only convert Notion to Markdown
|
| 287 |
console.log('📄 Notion conversion only mode');
|
| 288 |
-
await convertNotionToMarkdown(
|
| 289 |
|
| 290 |
} else {
|
| 291 |
// Full workflow
|
|
@@ -293,13 +221,13 @@ async function main() {
|
|
| 293 |
|
| 294 |
// Step 1: Convert Notion to Markdown
|
| 295 |
console.log('\n📄 Step 1: Converting Notion pages to Markdown...');
|
| 296 |
-
await convertNotionToMarkdown(
|
| 297 |
|
| 298 |
// Step 2: Convert Markdown to MDX with Notion metadata
|
| 299 |
console.log('\n📝 Step 2: Converting Markdown to MDX...');
|
| 300 |
-
const pagesConfig = readPagesConfig(
|
| 301 |
const firstPage = pagesConfig.pages && pagesConfig.pages.length > 0 ? pagesConfig.pages[0] : null;
|
| 302 |
-
const pageId =
|
| 303 |
await convertToMdx(config.output, config.output, pageId, config.token);
|
| 304 |
|
| 305 |
// Step 3: Copy to Astro content directory
|
|
|
|
| 6 |
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
| 7 |
import { convertNotionToMarkdown } from './notion-converter.mjs';
|
| 8 |
import { convertToMdx } from './mdx-converter.mjs';
|
|
|
|
| 9 |
|
| 10 |
+
// Load environment variables from .env file
|
| 11 |
+
config();
|
| 12 |
|
| 13 |
const __filename = fileURLToPath(import.meta.url);
|
| 14 |
const __dirname = dirname(__filename);
|
|
|
|
| 28 |
clean: false,
|
| 29 |
notionOnly: false,
|
| 30 |
mdxOnly: false,
|
| 31 |
+
token: process.env.NOTION_TOKEN
|
|
|
|
| 32 |
};
|
| 33 |
|
| 34 |
for (const arg of args) {
|
|
|
|
| 38 |
config.output = arg.split('=')[1];
|
| 39 |
} else if (arg.startsWith('--token=')) {
|
| 40 |
config.token = arg.split('=')[1];
|
|
|
|
|
|
|
| 41 |
} else if (arg === '--clean') {
|
| 42 |
config.clean = true;
|
| 43 |
} else if (arg === '--notion-only') {
|
|
|
|
| 123 |
}
|
| 124 |
}
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
function copyToAstroContent(outputDir) {
|
| 127 |
console.log('📋 Copying MDX files to Astro content directory...');
|
| 128 |
|
|
|
|
| 136 |
const mdxFiles = files.filter(file => file.endsWith('.mdx'));
|
| 137 |
if (mdxFiles.length > 0) {
|
| 138 |
const mdxFile = join(outputDir, mdxFiles[0]); // Take the first MDX file
|
| 139 |
+
copyFileSync(mdxFile, ASTRO_CONTENT_PATH);
|
|
|
|
|
|
|
| 140 |
console.log(` ✅ Copied MDX to ${ASTRO_CONTENT_PATH}`);
|
| 141 |
}
|
| 142 |
|
|
|
|
| 199 |
console.log('========================');
|
| 200 |
|
| 201 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
if (config.clean) {
|
| 203 |
console.log('🧹 Cleaning output directory...');
|
| 204 |
await cleanDirectory(config.output);
|
|
|
|
| 213 |
} else if (config.notionOnly) {
|
| 214 |
// Only convert Notion to Markdown
|
| 215 |
console.log('📄 Notion conversion only mode');
|
| 216 |
+
await convertNotionToMarkdown(config.input, config.output, config.token);
|
| 217 |
|
| 218 |
} else {
|
| 219 |
// Full workflow
|
|
|
|
| 221 |
|
| 222 |
// Step 1: Convert Notion to Markdown
|
| 223 |
console.log('\n📄 Step 1: Converting Notion pages to Markdown...');
|
| 224 |
+
await convertNotionToMarkdown(config.input, config.output, config.token);
|
| 225 |
|
| 226 |
// Step 2: Convert Markdown to MDX with Notion metadata
|
| 227 |
console.log('\n📝 Step 2: Converting Markdown to MDX...');
|
| 228 |
+
const pagesConfig = readPagesConfig(config.input);
|
| 229 |
const firstPage = pagesConfig.pages && pagesConfig.pages.length > 0 ? pagesConfig.pages[0] : null;
|
| 230 |
+
const pageId = firstPage ? firstPage.id : null;
|
| 231 |
await convertToMdx(config.output, config.output, pageId, config.token);
|
| 232 |
|
| 233 |
// Step 3: Copy to Astro content directory
|
app/scripts/notion-importer/input/pages.json
CHANGED
|
@@ -1,9 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
"id": "27877f1c9c9d804d9c82f7b3905578ff",
|
| 5 |
-
"title": "The Smol Training Guide",
|
| 6 |
-
"slug": "smol-training-guide"
|
| 7 |
-
}
|
| 8 |
-
]
|
| 9 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d51fba4ce9b05562f5df611a150e3cd702b487d2e608441318336556e0f248a
|
| 3 |
+
size 188
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/scripts/notion-importer/mdx-converter.mjs
CHANGED
|
@@ -122,12 +122,12 @@ function addComponentImports(content) {
|
|
| 122 |
}
|
| 123 |
|
| 124 |
/**
|
| 125 |
-
* Transform Notion images to
|
| 126 |
* @param {string} content - MDX content
|
| 127 |
-
* @returns {string} - Content with
|
| 128 |
*/
|
| 129 |
function transformImages(content) {
|
| 130 |
-
console.log(' 🖼️ Transforming images to
|
| 131 |
|
| 132 |
let hasImages = false;
|
| 133 |
|
|
@@ -163,8 +163,8 @@ function transformImages(content) {
|
|
| 163 |
: cleaned;
|
| 164 |
};
|
| 165 |
|
| 166 |
-
// Create
|
| 167 |
-
const
|
| 168 |
const cleanSrc = cleanSrcPath(src);
|
| 169 |
|
| 170 |
// Skip PDF URLs and external URLs - they should remain as links only
|
|
@@ -177,7 +177,7 @@ function transformImages(content) {
|
|
| 177 |
|
| 178 |
const varName = generateImageVarName(cleanSrc);
|
| 179 |
imageImports.set(cleanSrc, varName);
|
| 180 |
-
usedComponents.add('
|
| 181 |
|
| 182 |
const props = [];
|
| 183 |
props.push(`src={${varName}}`);
|
|
@@ -187,30 +187,30 @@ function transformImages(content) {
|
|
| 187 |
if (alt) props.push(`alt="${alt}"`);
|
| 188 |
if (caption) props.push(`caption={'${caption}'}`);
|
| 189 |
|
| 190 |
-
return `<
|
| 191 |
};
|
| 192 |
|
| 193 |
// Transform markdown images: 
|
| 194 |
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
| 195 |
const cleanSrc = cleanSrcPath(src);
|
| 196 |
-
const cleanAlt = cleanAltText(alt || '
|
| 197 |
hasImages = true;
|
| 198 |
|
| 199 |
-
return
|
| 200 |
});
|
| 201 |
|
| 202 |
// Transform images with captions (Notion sometimes adds captions as separate text)
|
| 203 |
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)\s*\n\s*([^\n]+)/g, (match, alt, src, caption) => {
|
| 204 |
const cleanSrc = cleanSrcPath(src);
|
| 205 |
-
const cleanAlt = cleanAltText(alt || '
|
| 206 |
const cleanCap = cleanCaption(caption);
|
| 207 |
hasImages = true;
|
| 208 |
|
| 209 |
-
return
|
| 210 |
});
|
| 211 |
|
| 212 |
if (hasImages) {
|
| 213 |
-
console.log(' ✅
|
| 214 |
}
|
| 215 |
|
| 216 |
return content;
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
/**
|
| 125 |
+
* Transform Notion images to Figure components
|
| 126 |
* @param {string} content - MDX content
|
| 127 |
+
* @returns {string} - Content with Figure components
|
| 128 |
*/
|
| 129 |
function transformImages(content) {
|
| 130 |
+
console.log(' 🖼️ Transforming images to Figure components...');
|
| 131 |
|
| 132 |
let hasImages = false;
|
| 133 |
|
|
|
|
| 163 |
: cleaned;
|
| 164 |
};
|
| 165 |
|
| 166 |
+
// Create Figure component with import
|
| 167 |
+
const createFigureComponent = (src, alt = '', caption = '') => {
|
| 168 |
const cleanSrc = cleanSrcPath(src);
|
| 169 |
|
| 170 |
// Skip PDF URLs and external URLs - they should remain as links only
|
|
|
|
| 177 |
|
| 178 |
const varName = generateImageVarName(cleanSrc);
|
| 179 |
imageImports.set(cleanSrc, varName);
|
| 180 |
+
usedComponents.add('Figure');
|
| 181 |
|
| 182 |
const props = [];
|
| 183 |
props.push(`src={${varName}}`);
|
|
|
|
| 187 |
if (alt) props.push(`alt="${alt}"`);
|
| 188 |
if (caption) props.push(`caption={'${caption}'}`);
|
| 189 |
|
| 190 |
+
return `<Figure\n ${props.join('\n ')}\n/>`;
|
| 191 |
};
|
| 192 |
|
| 193 |
// Transform markdown images: 
|
| 194 |
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
| 195 |
const cleanSrc = cleanSrcPath(src);
|
| 196 |
+
const cleanAlt = cleanAltText(alt || 'Figure');
|
| 197 |
hasImages = true;
|
| 198 |
|
| 199 |
+
return createFigureComponent(cleanSrc, cleanAlt);
|
| 200 |
});
|
| 201 |
|
| 202 |
// Transform images with captions (Notion sometimes adds captions as separate text)
|
| 203 |
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)\s*\n\s*([^\n]+)/g, (match, alt, src, caption) => {
|
| 204 |
const cleanSrc = cleanSrcPath(src);
|
| 205 |
+
const cleanAlt = cleanAltText(alt || 'Figure');
|
| 206 |
const cleanCap = cleanCaption(caption);
|
| 207 |
hasImages = true;
|
| 208 |
|
| 209 |
+
return createFigureComponent(cleanSrc, cleanAlt, cleanCap);
|
| 210 |
});
|
| 211 |
|
| 212 |
if (hasImages) {
|
| 213 |
+
console.log(' ✅ Figure components with imports will be created');
|
| 214 |
}
|
| 215 |
|
| 216 |
return content;
|
app/scripts/notion-importer/notion-converter.mjs
CHANGED
|
@@ -10,8 +10,8 @@ import { fileURLToPath } from 'url';
|
|
| 10 |
import { postProcessMarkdown } from './post-processor.mjs';
|
| 11 |
import { createCustomCodeRenderer } from './custom-code-renderer.mjs';
|
| 12 |
|
| 13 |
-
// Load environment variables from .env file
|
| 14 |
-
config(
|
| 15 |
|
| 16 |
const __filename = fileURLToPath(import.meta.url);
|
| 17 |
const __dirname = dirname(__filename);
|
|
|
|
| 10 |
import { postProcessMarkdown } from './post-processor.mjs';
|
| 11 |
import { createCustomCodeRenderer } from './custom-code-renderer.mjs';
|
| 12 |
|
| 13 |
+
// Load environment variables from .env file
|
| 14 |
+
config();
|
| 15 |
|
| 16 |
const __filename = fileURLToPath(import.meta.url);
|
| 17 |
const __dirname = dirname(__filename);
|
app/scripts/sync-template.mjs
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Template synchronization script for research-article-template
|
| 5 |
+
*
|
| 6 |
+
* This script:
|
| 7 |
+
* 1. Clones or updates the template repo in a temporary directory
|
| 8 |
+
* 2. Copies all files EXCEPT those in ./src/content which contain specific content
|
| 9 |
+
* 3. Preserves important local configuration files
|
| 10 |
+
* 4. Creates backups of files that will be overwritten
|
| 11 |
+
*
|
| 12 |
+
* Usage: npm run sync:template [--dry-run] [--backup] [--force]
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
import { execSync } from 'child_process';
|
| 16 |
+
import fs from 'fs/promises';
|
| 17 |
+
import path from 'path';
|
| 18 |
+
import { fileURLToPath } from 'url';
|
| 19 |
+
|
| 20 |
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 21 |
+
const APP_ROOT = path.resolve(__dirname, '..');
|
| 22 |
+
const PROJECT_ROOT = path.resolve(APP_ROOT, '..');
|
| 23 |
+
const TEMP_DIR = path.join(PROJECT_ROOT, '.temp-template-sync');
|
| 24 |
+
const TEMPLATE_REPO = 'https://huggingface.co/spaces/tfrere/research-article-template';
|
| 25 |
+
|
| 26 |
+
// Files and directories to PRESERVE (do not overwrite)
|
| 27 |
+
const PRESERVE_PATHS = [
|
| 28 |
+
// Project-specific content
|
| 29 |
+
'app/src/content',
|
| 30 |
+
|
| 31 |
+
// Public data (symlink to our data) - CRITICAL: preserve this symlink
|
| 32 |
+
'app/public/data',
|
| 33 |
+
|
| 34 |
+
// Local configuration
|
| 35 |
+
'app/package-lock.json',
|
| 36 |
+
'app/node_modules',
|
| 37 |
+
|
| 38 |
+
// Project-specific scripts (preserve our sync script)
|
| 39 |
+
'app/scripts/sync-template.mjs',
|
| 40 |
+
|
| 41 |
+
// Project configuration files
|
| 42 |
+
'README.md',
|
| 43 |
+
'tools',
|
| 44 |
+
|
| 45 |
+
// Backup and temporary files
|
| 46 |
+
'.backup-*',
|
| 47 |
+
'.temp-*',
|
| 48 |
+
|
| 49 |
+
// Git
|
| 50 |
+
'.git',
|
| 51 |
+
'.gitignore'
|
| 52 |
+
];
|
| 53 |
+
|
| 54 |
+
// Files to handle with caution (require confirmation)
|
| 55 |
+
const SENSITIVE_FILES = [
|
| 56 |
+
'app/package.json',
|
| 57 |
+
'app/astro.config.mjs',
|
| 58 |
+
'Dockerfile',
|
| 59 |
+
'nginx.conf'
|
| 60 |
+
];
|
| 61 |
+
|
| 62 |
+
const args = process.argv.slice(2);
|
| 63 |
+
const isDryRun = args.includes('--dry-run');
|
| 64 |
+
const shouldBackup = args.includes('--backup'); // Disabled by default, use --backup to enable
|
| 65 |
+
const isForce = args.includes('--force');
|
| 66 |
+
|
| 67 |
+
console.log('🔄 Template synchronization script for research-article-template');
|
| 68 |
+
console.log(`📁 Working directory: ${PROJECT_ROOT}`);
|
| 69 |
+
console.log(`🎯 Template source: ${TEMPLATE_REPO}`);
|
| 70 |
+
if (isDryRun) console.log('🔍 DRY-RUN mode enabled - no files will be modified');
|
| 71 |
+
if (shouldBackup) console.log('💾 Backup enabled');
|
| 72 |
+
if (!shouldBackup) console.log('🚫 Backup disabled (use --backup to enable)');
|
| 73 |
+
console.log('');
|
| 74 |
+
|
| 75 |
+
async function executeCommand(command, options = {}) {
|
| 76 |
+
try {
|
| 77 |
+
if (isDryRun && !options.allowInDryRun) {
|
| 78 |
+
console.log(`[DRY-RUN] Command: ${command}`);
|
| 79 |
+
return '';
|
| 80 |
+
}
|
| 81 |
+
console.log(`$ ${command}`);
|
| 82 |
+
const result = execSync(command, {
|
| 83 |
+
encoding: 'utf8',
|
| 84 |
+
cwd: options.cwd || PROJECT_ROOT,
|
| 85 |
+
stdio: options.quiet ? 'pipe' : 'inherit'
|
| 86 |
+
});
|
| 87 |
+
return result;
|
| 88 |
+
} catch (error) {
|
| 89 |
+
console.error(`❌ Error during execution: ${command}`);
|
| 90 |
+
console.error(error.message);
|
| 91 |
+
throw error;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
async function pathExists(filePath) {
|
| 96 |
+
try {
|
| 97 |
+
await fs.access(filePath);
|
| 98 |
+
return true;
|
| 99 |
+
} catch {
|
| 100 |
+
return false;
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async function isPathPreserved(relativePath) {
|
| 105 |
+
return PRESERVE_PATHS.some(preserve =>
|
| 106 |
+
relativePath === preserve ||
|
| 107 |
+
relativePath.startsWith(preserve + '/')
|
| 108 |
+
);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
async function createBackup(filePath) {
|
| 112 |
+
if (!shouldBackup || isDryRun) return;
|
| 113 |
+
|
| 114 |
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
| 115 |
+
const backupPath = `${filePath}.backup-${timestamp}`;
|
| 116 |
+
|
| 117 |
+
try {
|
| 118 |
+
await fs.copyFile(filePath, backupPath);
|
| 119 |
+
console.log(`💾 Backup created: ${path.relative(PROJECT_ROOT, backupPath)}`);
|
| 120 |
+
} catch (error) {
|
| 121 |
+
console.warn(`⚠️ Unable to create backup for ${filePath}: ${error.message}`);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
async function syncFile(sourcePath, targetPath) {
|
| 126 |
+
const relativeTarget = path.relative(PROJECT_ROOT, targetPath);
|
| 127 |
+
|
| 128 |
+
// Check if the file should be preserved
|
| 129 |
+
if (await isPathPreserved(relativeTarget)) {
|
| 130 |
+
console.log(`🔒 PRESERVED: ${relativeTarget}`);
|
| 131 |
+
return;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Check if it's a sensitive file
|
| 135 |
+
if (SENSITIVE_FILES.includes(relativeTarget)) {
|
| 136 |
+
if (!isForce) {
|
| 137 |
+
console.log(`⚠️ SENSITIVE (ignored): ${relativeTarget} (use --force to overwrite)`);
|
| 138 |
+
return;
|
| 139 |
+
} else {
|
| 140 |
+
console.log(`⚠️ SENSITIVE (forced): ${relativeTarget}`);
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Check if target file is a symbolic link to preserve
|
| 145 |
+
if (await pathExists(targetPath)) {
|
| 146 |
+
try {
|
| 147 |
+
const targetStats = await fs.lstat(targetPath);
|
| 148 |
+
if (targetStats.isSymbolicLink()) {
|
| 149 |
+
console.log(`🔗 SYMLINK TARGET (preserved): ${relativeTarget}`);
|
| 150 |
+
return;
|
| 151 |
+
}
|
| 152 |
+
} catch (error) {
|
| 153 |
+
console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`);
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Create backup if file already exists (and is not a symbolic link)
|
| 158 |
+
if (await pathExists(targetPath)) {
|
| 159 |
+
try {
|
| 160 |
+
const stats = await fs.lstat(targetPath);
|
| 161 |
+
if (!stats.isSymbolicLink()) {
|
| 162 |
+
await createBackup(targetPath);
|
| 163 |
+
}
|
| 164 |
+
} catch (error) {
|
| 165 |
+
console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
if (isDryRun) {
|
| 170 |
+
console.log(`[DRY-RUN] COPY: ${relativeTarget}`);
|
| 171 |
+
return;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// Assurer que le répertoire parent existe
|
| 175 |
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
| 176 |
+
|
| 177 |
+
// Check if source is a symbolic link
|
| 178 |
+
try {
|
| 179 |
+
const sourceStats = await fs.lstat(sourcePath);
|
| 180 |
+
if (sourceStats.isSymbolicLink()) {
|
| 181 |
+
console.log(`🔗 SYMLINK SOURCE (ignored): ${relativeTarget}`);
|
| 182 |
+
return;
|
| 183 |
+
}
|
| 184 |
+
} catch (error) {
|
| 185 |
+
console.warn(`⚠️ Unable to check source ${sourcePath}: ${error.message}`);
|
| 186 |
+
return;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Remove target file if it exists (to handle symbolic links)
|
| 190 |
+
if (await pathExists(targetPath)) {
|
| 191 |
+
await fs.rm(targetPath, { recursive: true, force: true });
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
// Copier le fichier
|
| 195 |
+
await fs.copyFile(sourcePath, targetPath);
|
| 196 |
+
console.log(`✅ COPIED: ${relativeTarget}`);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
async function syncDirectory(sourceDir, targetDir) {
|
| 200 |
+
const items = await fs.readdir(sourceDir, { withFileTypes: true });
|
| 201 |
+
|
| 202 |
+
for (const item of items) {
|
| 203 |
+
const sourcePath = path.join(sourceDir, item.name);
|
| 204 |
+
const targetPath = path.join(targetDir, item.name);
|
| 205 |
+
const relativeTarget = path.relative(PROJECT_ROOT, targetPath);
|
| 206 |
+
|
| 207 |
+
if (await isPathPreserved(relativeTarget)) {
|
| 208 |
+
console.log(`🔒 DOSSIER PRÉSERVÉ: ${relativeTarget}/`);
|
| 209 |
+
continue;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
if (item.isDirectory()) {
|
| 213 |
+
if (!isDryRun) {
|
| 214 |
+
await fs.mkdir(targetPath, { recursive: true });
|
| 215 |
+
}
|
| 216 |
+
await syncDirectory(sourcePath, targetPath);
|
| 217 |
+
} else {
|
| 218 |
+
await syncFile(sourcePath, targetPath);
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
async function cloneOrUpdateTemplate() {
|
| 224 |
+
console.log('📥 Fetching template...');
|
| 225 |
+
|
| 226 |
+
// Nettoyer le dossier temporaire s'il existe
|
| 227 |
+
if (await pathExists(TEMP_DIR)) {
|
| 228 |
+
await fs.rm(TEMP_DIR, { recursive: true, force: true });
|
| 229 |
+
if (isDryRun) {
|
| 230 |
+
console.log(`[DRY-RUN] Suppression: ${TEMP_DIR}`);
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Clone template repo (even in dry-run to be able to compare)
|
| 235 |
+
await executeCommand(`git clone ${TEMPLATE_REPO} "${TEMP_DIR}"`, { allowInDryRun: true });
|
| 236 |
+
|
| 237 |
+
return TEMP_DIR;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
async function ensureDataSymlink() {
|
| 241 |
+
const dataSymlinkPath = path.join(APP_ROOT, 'public', 'data');
|
| 242 |
+
const dataSourcePath = path.join(APP_ROOT, 'src', 'content', 'assets', 'data');
|
| 243 |
+
|
| 244 |
+
// Check if symlink exists and is correct
|
| 245 |
+
if (await pathExists(dataSymlinkPath)) {
|
| 246 |
+
try {
|
| 247 |
+
const stats = await fs.lstat(dataSymlinkPath);
|
| 248 |
+
if (stats.isSymbolicLink()) {
|
| 249 |
+
const target = await fs.readlink(dataSymlinkPath);
|
| 250 |
+
const expectedTarget = path.relative(path.dirname(dataSymlinkPath), dataSourcePath);
|
| 251 |
+
if (target === expectedTarget) {
|
| 252 |
+
console.log('🔗 Data symlink is correct');
|
| 253 |
+
return;
|
| 254 |
+
} else {
|
| 255 |
+
console.log(`⚠️ Data symlink points to wrong target: ${target} (expected: ${expectedTarget})`);
|
| 256 |
+
}
|
| 257 |
+
} else {
|
| 258 |
+
console.log('⚠️ app/public/data exists but is not a symlink');
|
| 259 |
+
}
|
| 260 |
+
} catch (error) {
|
| 261 |
+
console.log(`⚠️ Error checking symlink: ${error.message}`);
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// Recreate symlink
|
| 266 |
+
if (!isDryRun) {
|
| 267 |
+
if (await pathExists(dataSymlinkPath)) {
|
| 268 |
+
await fs.rm(dataSymlinkPath, { recursive: true, force: true });
|
| 269 |
+
}
|
| 270 |
+
await fs.symlink(path.relative(path.dirname(dataSymlinkPath), dataSourcePath), dataSymlinkPath);
|
| 271 |
+
console.log('✅ Data symlink recreated');
|
| 272 |
+
} else {
|
| 273 |
+
console.log('[DRY-RUN] Would recreate data symlink');
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
async function showSummary(templateDir) {
|
| 278 |
+
console.log('\n📊 SYNCHRONIZATION SUMMARY');
|
| 279 |
+
console.log('================================');
|
| 280 |
+
|
| 281 |
+
console.log('\n🔒 Preserved files/directories:');
|
| 282 |
+
for (const preserve of PRESERVE_PATHS) {
|
| 283 |
+
const fullPath = path.join(PROJECT_ROOT, preserve);
|
| 284 |
+
if (await pathExists(fullPath)) {
|
| 285 |
+
console.log(` ✓ ${preserve}`);
|
| 286 |
+
} else {
|
| 287 |
+
console.log(` - ${preserve} (n'existe pas)`);
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
console.log('\n⚠️ Sensitive files (require --force):');
|
| 292 |
+
for (const sensitive of SENSITIVE_FILES) {
|
| 293 |
+
const fullPath = path.join(PROJECT_ROOT, sensitive);
|
| 294 |
+
if (await pathExists(fullPath)) {
|
| 295 |
+
console.log(` ! ${sensitive}`);
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
if (isDryRun) {
|
| 300 |
+
console.log('\n🔍 To execute for real: npm run sync:template');
|
| 301 |
+
console.log('🔧 To force sensitive files: npm run sync:template -- --force');
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
async function cleanup() {
|
| 306 |
+
console.log('\n🧹 Cleaning up...');
|
| 307 |
+
if (await pathExists(TEMP_DIR)) {
|
| 308 |
+
if (!isDryRun) {
|
| 309 |
+
await fs.rm(TEMP_DIR, { recursive: true, force: true });
|
| 310 |
+
}
|
| 311 |
+
console.log(`🗑️ Temporary directory removed: ${TEMP_DIR}`);
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
async function main() {
|
| 316 |
+
try {
|
| 317 |
+
// Verify we're in the correct directory
|
| 318 |
+
const packageJsonPath = path.join(APP_ROOT, 'package.json');
|
| 319 |
+
if (!(await pathExists(packageJsonPath))) {
|
| 320 |
+
throw new Error(`Package.json not found in ${APP_ROOT}. Are you in the correct directory?`);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// Clone the template
|
| 324 |
+
const templateDir = await cloneOrUpdateTemplate();
|
| 325 |
+
|
| 326 |
+
// Synchroniser
|
| 327 |
+
console.log('\n🔄 Synchronisation en cours...');
|
| 328 |
+
await syncDirectory(templateDir, PROJECT_ROOT);
|
| 329 |
+
|
| 330 |
+
// S'assurer que le lien symbolique des données est correct
|
| 331 |
+
console.log('\n🔗 Vérification du lien symbolique des données...');
|
| 332 |
+
await ensureDataSymlink();
|
| 333 |
+
|
| 334 |
+
// Afficher le résumé
|
| 335 |
+
await showSummary(templateDir);
|
| 336 |
+
|
| 337 |
+
console.log('\n✅ Synchronization completed!');
|
| 338 |
+
|
| 339 |
+
} catch (error) {
|
| 340 |
+
console.error('\n❌ Error during synchronization:');
|
| 341 |
+
console.error(error.message);
|
| 342 |
+
process.exit(1);
|
| 343 |
+
} finally {
|
| 344 |
+
await cleanup();
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// Signal handling to clean up on interruption
|
| 349 |
+
process.on('SIGINT', async () => {
|
| 350 |
+
console.log('\n\n⚠️ Interruption detected, cleaning up...');
|
| 351 |
+
await cleanup();
|
| 352 |
+
process.exit(1);
|
| 353 |
+
});
|
| 354 |
+
|
| 355 |
+
process.on('SIGTERM', async () => {
|
| 356 |
+
console.log('\n\n⚠️ Shutdown requested, cleaning up...');
|
| 357 |
+
await cleanup();
|
| 358 |
+
process.exit(1);
|
| 359 |
+
});
|
| 360 |
+
|
| 361 |
+
main();
|
app/src/components/Glossary.astro
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
interface Props {
|
| 3 |
+
/** The word or term to define */
|
| 4 |
+
term: string;
|
| 5 |
+
/** The definition of the term */
|
| 6 |
+
definition: string;
|
| 7 |
+
/** Optional CSS class to apply to the term */
|
| 8 |
+
class?: string;
|
| 9 |
+
/** Optional style to apply to the term */
|
| 10 |
+
style?: string;
|
| 11 |
+
/** Tooltip position (top, bottom, left, right) */
|
| 12 |
+
position?: "top" | "bottom" | "left" | "right";
|
| 13 |
+
/** Delay before showing tooltip in ms */
|
| 14 |
+
delay?: number;
|
| 15 |
+
/** Disable tooltip on mobile */
|
| 16 |
+
disableOnMobile?: boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const {
|
| 20 |
+
term,
|
| 21 |
+
definition,
|
| 22 |
+
class: className = "",
|
| 23 |
+
style: inlineStyle = "",
|
| 24 |
+
position = "top",
|
| 25 |
+
delay = 300,
|
| 26 |
+
disableOnMobile = false,
|
| 27 |
+
} = Astro.props as Props;
|
| 28 |
+
|
| 29 |
+
// Generate a unique ID for this component
|
| 30 |
+
const tooltipId = `glossary-${Math.random().toString(36).slice(2)}`;
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
<div class="glossary-container" data-glossary-container-id={tooltipId}>
|
| 34 |
+
<span
|
| 35 |
+
class={`glossary-term ${className}`}
|
| 36 |
+
style={inlineStyle}
|
| 37 |
+
data-glossary-term={term}
|
| 38 |
+
data-glossary-definition={definition}
|
| 39 |
+
data-glossary-position={position}
|
| 40 |
+
data-glossary-delay={delay}
|
| 41 |
+
data-glossary-disable-mobile={disableOnMobile}
|
| 42 |
+
data-glossary-id={tooltipId}
|
| 43 |
+
tabindex="0"
|
| 44 |
+
role="button"
|
| 45 |
+
aria-describedby={`${tooltipId}-tooltip`}
|
| 46 |
+
>
|
| 47 |
+
{term}
|
| 48 |
+
</span>
|
| 49 |
+
|
| 50 |
+
<div
|
| 51 |
+
id={`${tooltipId}-tooltip`}
|
| 52 |
+
class="glossary-tooltip"
|
| 53 |
+
data-glossary-tooltip-id={tooltipId}
|
| 54 |
+
data-position={position}
|
| 55 |
+
role="tooltip"
|
| 56 |
+
aria-hidden="true"
|
| 57 |
+
>
|
| 58 |
+
<div class="glossary-tooltip__content">
|
| 59 |
+
<div class="glossary-tooltip__term">{term}</div>
|
| 60 |
+
<div class="glossary-tooltip__definition">{definition}</div>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="glossary-tooltip__arrow"></div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<script is:inline>
|
| 67 |
+
// Global script for all Glossary tooltips
|
| 68 |
+
if (!window.glossaryInitialized) {
|
| 69 |
+
window.glossaryInitialized = true;
|
| 70 |
+
|
| 71 |
+
function initAllGlossaryTooltips() {
|
| 72 |
+
const glossaryTerms = document.querySelectorAll(".glossary-term");
|
| 73 |
+
|
| 74 |
+
glossaryTerms.forEach((termElement) => {
|
| 75 |
+
const tooltipElement =
|
| 76 |
+
termElement.parentElement.querySelector(".glossary-tooltip");
|
| 77 |
+
|
| 78 |
+
if (!tooltipElement) return;
|
| 79 |
+
|
| 80 |
+
const term = termElement.getAttribute("data-glossary-term");
|
| 81 |
+
const definition = termElement.getAttribute("data-glossary-definition");
|
| 82 |
+
|
| 83 |
+
if (!term || !definition) return;
|
| 84 |
+
|
| 85 |
+
// Fonction pour afficher le tooltip au niveau de la souris
|
| 86 |
+
const showTooltip = (event) => {
|
| 87 |
+
tooltipElement.style.display = "block";
|
| 88 |
+
tooltipElement.style.opacity = "1";
|
| 89 |
+
tooltipElement.style.position = "fixed";
|
| 90 |
+
tooltipElement.style.top = event.clientY + 10 + "px";
|
| 91 |
+
tooltipElement.style.left = event.clientX + 10 + "px";
|
| 92 |
+
tooltipElement.style.zIndex = "9999";
|
| 93 |
+
tooltipElement.style.pointerEvents = "none";
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
const hideTooltip = () => {
|
| 97 |
+
tooltipElement.style.display = "none";
|
| 98 |
+
tooltipElement.style.opacity = "0";
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
// Add events
|
| 102 |
+
termElement.addEventListener("mouseenter", showTooltip);
|
| 103 |
+
termElement.addEventListener("mouseleave", hideTooltip);
|
| 104 |
+
termElement.addEventListener("mousemove", showTooltip);
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Initialize when DOM is ready
|
| 109 |
+
if (document.readyState === "loading") {
|
| 110 |
+
document.addEventListener("DOMContentLoaded", initAllGlossaryTooltips);
|
| 111 |
+
} else {
|
| 112 |
+
initAllGlossaryTooltips();
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Observe DOM changes for new elements
|
| 116 |
+
if (window.MutationObserver) {
|
| 117 |
+
const observer = new MutationObserver((mutations) => {
|
| 118 |
+
mutations.forEach((mutation) => {
|
| 119 |
+
if (mutation.type === "childList") {
|
| 120 |
+
mutation.addedNodes.forEach((node) => {
|
| 121 |
+
if (
|
| 122 |
+
node.nodeType === 1 &&
|
| 123 |
+
node.querySelector &&
|
| 124 |
+
node.querySelector(".glossary-term")
|
| 125 |
+
) {
|
| 126 |
+
initAllGlossaryTooltips();
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
});
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
observer.observe(document.body, {
|
| 134 |
+
childList: true,
|
| 135 |
+
subtree: true,
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
</script>
|
| 140 |
+
|
| 141 |
+
<style>
|
| 142 |
+
/* ============================================================================ */
|
| 143 |
+
/* Glossary Component */
|
| 144 |
+
/* ============================================================================ */
|
| 145 |
+
|
| 146 |
+
.glossary-container {
|
| 147 |
+
display: inline;
|
| 148 |
+
position: relative;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.glossary-term {
|
| 152 |
+
color: var(--primary-color) !important;
|
| 153 |
+
text-decoration: none !important;
|
| 154 |
+
background: color-mix(in srgb, var(--primary-color) 15%, transparent);
|
| 155 |
+
border-bottom: 1px dashed
|
| 156 |
+
color-mix(in srgb, var(--primary-color) 100%, transparent) !important;
|
| 157 |
+
cursor: help;
|
| 158 |
+
transition: all 0.2s ease;
|
| 159 |
+
border-radius: 3px;
|
| 160 |
+
margin: 0 2px;
|
| 161 |
+
padding: 4px 8px;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.glossary-term:hover,
|
| 165 |
+
.glossary-term:focus {
|
| 166 |
+
color: var(--primary-color-hover) !important;
|
| 167 |
+
text-decoration: none !important;
|
| 168 |
+
background: color-mix(in srgb, var(--primary-color) 20%, transparent);
|
| 169 |
+
outline: none;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.glossary-term:focus {
|
| 173 |
+
box-shadow: 0 0 0 2px
|
| 174 |
+
color-mix(in srgb, var(--primary-color) 20%, transparent);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.glossary-tooltip {
|
| 178 |
+
position: fixed;
|
| 179 |
+
top: -9999px;
|
| 180 |
+
left: -9999px;
|
| 181 |
+
z-index: var(--z-tooltip);
|
| 182 |
+
opacity: 0;
|
| 183 |
+
transform: translateY(-4px);
|
| 184 |
+
transition:
|
| 185 |
+
opacity 0.2s ease,
|
| 186 |
+
transform 0.2s ease;
|
| 187 |
+
pointer-events: none;
|
| 188 |
+
max-width: 300px;
|
| 189 |
+
min-width: 200px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.glossary-tooltip.is-visible {
|
| 193 |
+
opacity: 1;
|
| 194 |
+
transform: translateY(0);
|
| 195 |
+
pointer-events: auto;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.glossary-tooltip__content {
|
| 199 |
+
background: var(--surface-bg);
|
| 200 |
+
border: 1px solid var(--border-color);
|
| 201 |
+
border-radius: 8px;
|
| 202 |
+
padding: 12px 16px;
|
| 203 |
+
box-shadow:
|
| 204 |
+
0 8px 32px rgba(0, 0, 0, 0.12),
|
| 205 |
+
0 2px 8px rgba(0, 0, 0, 0.06);
|
| 206 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.glossary-tooltip__term {
|
| 210 |
+
font-weight: 600;
|
| 211 |
+
font-size: 14px;
|
| 212 |
+
color: var(--primary-color);
|
| 213 |
+
margin-bottom: 4px;
|
| 214 |
+
line-height: 1.3;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.glossary-tooltip__definition {
|
| 218 |
+
font-size: 13px;
|
| 219 |
+
color: var(--text-color);
|
| 220 |
+
line-height: 1.4;
|
| 221 |
+
margin: 0;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.glossary-tooltip__arrow {
|
| 225 |
+
position: absolute;
|
| 226 |
+
width: 0;
|
| 227 |
+
height: 0;
|
| 228 |
+
border: 6px solid transparent;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/* Arrow positioning */
|
| 232 |
+
.glossary-tooltip[data-position="top"] .glossary-tooltip__arrow {
|
| 233 |
+
bottom: -6px;
|
| 234 |
+
left: 50%;
|
| 235 |
+
transform: translateX(-50%);
|
| 236 |
+
border-top-color: var(--border-color);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.glossary-tooltip[data-position="top"] .glossary-tooltip__arrow::after {
|
| 240 |
+
content: "";
|
| 241 |
+
position: absolute;
|
| 242 |
+
top: -7px;
|
| 243 |
+
left: -6px;
|
| 244 |
+
border: 6px solid transparent;
|
| 245 |
+
border-top-color: var(--surface-bg);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.glossary-tooltip[data-position="bottom"] .glossary-tooltip__arrow {
|
| 249 |
+
top: -6px;
|
| 250 |
+
left: 50%;
|
| 251 |
+
transform: translateX(-50%);
|
| 252 |
+
border-bottom-color: var(--border-color);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.glossary-tooltip[data-position="bottom"] .glossary-tooltip__arrow::after {
|
| 256 |
+
content: "";
|
| 257 |
+
position: absolute;
|
| 258 |
+
top: -5px;
|
| 259 |
+
left: -6px;
|
| 260 |
+
border: 6px solid transparent;
|
| 261 |
+
border-bottom-color: var(--surface-bg);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.glossary-tooltip[data-position="left"] .glossary-tooltip__arrow {
|
| 265 |
+
right: -6px;
|
| 266 |
+
top: 50%;
|
| 267 |
+
transform: translateY(-50%);
|
| 268 |
+
border-left-color: var(--border-color);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.glossary-tooltip[data-position="left"] .glossary-tooltip__arrow::after {
|
| 272 |
+
content: "";
|
| 273 |
+
position: absolute;
|
| 274 |
+
top: -6px;
|
| 275 |
+
left: -7px;
|
| 276 |
+
border: 6px solid transparent;
|
| 277 |
+
border-left-color: var(--surface-bg);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.glossary-tooltip[data-position="right"] .glossary-tooltip__arrow {
|
| 281 |
+
left: -6px;
|
| 282 |
+
top: 50%;
|
| 283 |
+
transform: translateY(-50%);
|
| 284 |
+
border-right-color: var(--border-color);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.glossary-tooltip[data-position="right"] .glossary-tooltip__arrow::after {
|
| 288 |
+
content: "";
|
| 289 |
+
position: absolute;
|
| 290 |
+
top: -6px;
|
| 291 |
+
left: -5px;
|
| 292 |
+
border: 6px solid transparent;
|
| 293 |
+
border-right-color: var(--surface-bg);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Mode sombre */
|
| 297 |
+
[data-theme="dark"] .glossary-tooltip__content {
|
| 298 |
+
background: var(--surface-bg);
|
| 299 |
+
border-color: var(--border-color);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
[data-theme="dark"] .glossary-tooltip__term {
|
| 303 |
+
color: var(--primary-color);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
[data-theme="dark"] .glossary-tooltip__definition {
|
| 307 |
+
color: var(--text-color);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* Responsive - hide on mobile if disabled */
|
| 311 |
+
@media (max-width: 768px) {
|
| 312 |
+
.glossary-term[data-glossary-disable-mobile="true"] {
|
| 313 |
+
border-bottom: none;
|
| 314 |
+
color: inherit;
|
| 315 |
+
cursor: default;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.glossary-term[data-glossary-disable-mobile="true"]:hover,
|
| 319 |
+
.glossary-term[data-glossary-disable-mobile="true"]:focus {
|
| 320 |
+
background: none;
|
| 321 |
+
color: inherit;
|
| 322 |
+
border-bottom: none;
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/* Accessibility improvement */
|
| 327 |
+
@media (prefers-reduced-motion: reduce) {
|
| 328 |
+
.glossary-tooltip {
|
| 329 |
+
transition: none;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.glossary-term {
|
| 333 |
+
transition: none;
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
</style>
|
app/src/components/Hero.astro
CHANGED
|
@@ -12,6 +12,7 @@ interface Props {
|
|
| 12 |
affiliation?: string; // legacy single affiliation
|
| 13 |
published?: string;
|
| 14 |
doi?: string;
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
const {
|
|
@@ -23,6 +24,7 @@ const {
|
|
| 23 |
affiliation,
|
| 24 |
published,
|
| 25 |
doi,
|
|
|
|
| 26 |
} = Astro.props as Props;
|
| 27 |
|
| 28 |
type Author = { name: string; url?: string; affiliationIndices?: number[] };
|
|
@@ -111,12 +113,11 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 111 |
shouldShowAffiliationSupers &&
|
| 112 |
Array.isArray(a.affiliationIndices) &&
|
| 113 |
a.affiliationIndices.length ? (
|
| 114 |
-
<sup>{a.affiliationIndices.join(",")}</sup>
|
| 115 |
) : null;
|
| 116 |
return (
|
| 117 |
<li>
|
| 118 |
-
{a.url ? <a href={a.url}>{a.name}</a> : a.name}
|
| 119 |
-
{supers}
|
| 120 |
</li>
|
| 121 |
);
|
| 122 |
})}
|
|
@@ -126,7 +127,7 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 126 |
}
|
| 127 |
{
|
| 128 |
Array.isArray(affiliations) && affiliations.length > 0 && (
|
| 129 |
-
<div class="meta-container-cell">
|
| 130 |
<h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3>
|
| 131 |
{hasMultipleAffiliations ? (
|
| 132 |
<ol class="affiliations">
|
|
@@ -162,7 +163,7 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 162 |
}
|
| 163 |
{
|
| 164 |
(!affiliations || affiliations.length === 0) && affiliation && (
|
| 165 |
-
<div class="meta-container-cell">
|
| 166 |
<h3>Affiliation</h3>
|
| 167 |
<p>{affiliation}</p>
|
| 168 |
</div>
|
|
@@ -183,26 +184,170 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 183 |
</div>
|
| 184 |
)} -->
|
| 185 |
<div class="meta-container-cell meta-container-cell--pdf">
|
| 186 |
-
<
|
| 187 |
-
|
| 188 |
-
<
|
| 189 |
-
class="
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
</div>
|
| 198 |
</div>
|
| 199 |
</header>
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
<style>
|
| 202 |
/* Hero (full-width) */
|
| 203 |
.hero {
|
| 204 |
width: 100%;
|
| 205 |
-
padding: 48px 16px
|
| 206 |
text-align: center;
|
| 207 |
}
|
| 208 |
.hero-title {
|
|
@@ -259,6 +404,7 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 259 |
display: flex;
|
| 260 |
flex-direction: column;
|
| 261 |
gap: 8px;
|
|
|
|
| 262 |
}
|
| 263 |
.meta-container-cell h3 {
|
| 264 |
margin: 0;
|
|
@@ -275,6 +421,12 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 275 |
margin: 0;
|
| 276 |
list-style-type: none;
|
| 277 |
padding-left: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
.affiliations {
|
| 280 |
margin: 0;
|
|
@@ -289,9 +441,152 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 289 |
row-gap: 12px;
|
| 290 |
}
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
@media print {
|
| 293 |
.meta-container-cell--pdf {
|
| 294 |
display: none !important;
|
| 295 |
}
|
| 296 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
</style>
|
|
|
|
| 12 |
affiliation?: string; // legacy single affiliation
|
| 13 |
published?: string;
|
| 14 |
doi?: string;
|
| 15 |
+
pdfProOnly?: boolean; // Gate PDF download to Pro users only
|
| 16 |
}
|
| 17 |
|
| 18 |
const {
|
|
|
|
| 24 |
affiliation,
|
| 25 |
published,
|
| 26 |
doi,
|
| 27 |
+
pdfProOnly = false,
|
| 28 |
} = Astro.props as Props;
|
| 29 |
|
| 30 |
type Author = { name: string; url?: string; affiliationIndices?: number[] };
|
|
|
|
| 113 |
shouldShowAffiliationSupers &&
|
| 114 |
Array.isArray(a.affiliationIndices) &&
|
| 115 |
a.affiliationIndices.length ? (
|
| 116 |
+
<sup>{a.affiliationIndices.join(", ")}</sup>
|
| 117 |
) : null;
|
| 118 |
return (
|
| 119 |
<li>
|
| 120 |
+
{a.url ? <a href={a.url}>{a.name}</a> : a.name}{supers}{i < normalizedAuthors.length - 1 && <span set:html=", " />}
|
|
|
|
| 121 |
</li>
|
| 122 |
);
|
| 123 |
})}
|
|
|
|
| 127 |
}
|
| 128 |
{
|
| 129 |
Array.isArray(affiliations) && affiliations.length > 0 && (
|
| 130 |
+
<div class="meta-container-cell meta-container-cell--affiliations">
|
| 131 |
<h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3>
|
| 132 |
{hasMultipleAffiliations ? (
|
| 133 |
<ol class="affiliations">
|
|
|
|
| 163 |
}
|
| 164 |
{
|
| 165 |
(!affiliations || affiliations.length === 0) && affiliation && (
|
| 166 |
+
<div class="meta-container-cell meta-container-cell--affiliations">
|
| 167 |
<h3>Affiliation</h3>
|
| 168 |
<p>{affiliation}</p>
|
| 169 |
</div>
|
|
|
|
| 184 |
</div>
|
| 185 |
)} -->
|
| 186 |
<div class="meta-container-cell meta-container-cell--pdf">
|
| 187 |
+
<div class="pdf-header-wrapper">
|
| 188 |
+
<h3>PDF</h3>
|
| 189 |
+
<span class="pro-badge-wrapper" style="display: none;">
|
| 190 |
+
<span class="pro-badge-prefix">- you are</span>
|
| 191 |
+
<span class="pro-badge">PRO</span>
|
| 192 |
+
</span>
|
| 193 |
+
<span class="pro-only-label" style="display: none;">
|
| 194 |
+
<span class="pro-only-dash">-</span>
|
| 195 |
+
<span class="pro-only-text">pro only</span>
|
| 196 |
+
<svg class="pro-only-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 197 |
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
| 198 |
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
| 199 |
+
</svg>
|
| 200 |
+
</span>
|
| 201 |
+
</div>
|
| 202 |
+
<div id="pdf-download-container" data-pdf-pro-only={pdfProOnly.toString()}>
|
| 203 |
+
<p class="pdf-loading">Checking access...</p>
|
| 204 |
+
<p class="pdf-pro-only" style="display: none;">
|
| 205 |
+
<a
|
| 206 |
+
class="button"
|
| 207 |
+
href={`/${pdfFilename}`}
|
| 208 |
+
download={pdfFilename}
|
| 209 |
+
aria-label={`Download PDF ${pdfFilename}`}
|
| 210 |
+
>
|
| 211 |
+
Download PDF
|
| 212 |
+
</a>
|
| 213 |
+
</p>
|
| 214 |
+
<div class="pdf-locked" style="display: none;">
|
| 215 |
+
<a
|
| 216 |
+
class="button button-locked"
|
| 217 |
+
href="https://huggingface.co/subscribe/pro"
|
| 218 |
+
target="_blank"
|
| 219 |
+
rel="noopener noreferrer"
|
| 220 |
+
>
|
| 221 |
+
<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" fill="none">
|
| 222 |
+
<path d="M6.48 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86A5.3 5.3 0 0 0 7.99 7.5a3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86A5.3 5.3 0 0 0 4.01 4.5a3.95 3.95 0 0 0 1.51-3.24h.96Z" fill="currentColor"></path>
|
| 223 |
+
</svg>
|
| 224 |
+
<span class="locked-title">Subscribe to Pro</span>
|
| 225 |
+
</a>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
</div>
|
| 229 |
</div>
|
| 230 |
</header>
|
| 231 |
|
| 232 |
+
<script>
|
| 233 |
+
// PDF access control for Pro users only
|
| 234 |
+
|
| 235 |
+
// ⚙️ Configuration for local development
|
| 236 |
+
const LOCAL_IS_PRO = false; // Set to true to test Pro access locally
|
| 237 |
+
|
| 238 |
+
const FALLBACK_TIMEOUT_MS = 3000;
|
| 239 |
+
let userPlanChecked = false;
|
| 240 |
+
|
| 241 |
+
// Check if PDF Pro gating is enabled
|
| 242 |
+
const pdfContainer = document.querySelector("#pdf-download-container") as HTMLElement;
|
| 243 |
+
const pdfProOnly = pdfContainer?.getAttribute("data-pdf-pro-only") === "true";
|
| 244 |
+
|
| 245 |
+
/**
|
| 246 |
+
* Check if user has Pro access
|
| 247 |
+
* Isolated logic for Pro user verification
|
| 248 |
+
* Expected plan structure: { user: "pro", org: "enterprise" }
|
| 249 |
+
*/
|
| 250 |
+
function isProUser(plan: any): boolean {
|
| 251 |
+
if (!plan) return false;
|
| 252 |
+
|
| 253 |
+
// Check if user property is "pro"
|
| 254 |
+
return plan.user === "pro";
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
/**
|
| 258 |
+
* Update UI based on user's Pro status
|
| 259 |
+
*/
|
| 260 |
+
function updatePdfAccess(isPro: boolean) {
|
| 261 |
+
const loadingEl = document.querySelector(".pdf-loading") as HTMLElement;
|
| 262 |
+
const proOnlyEl = document.querySelector(".pdf-pro-only") as HTMLElement;
|
| 263 |
+
const lockedEl = document.querySelector(".pdf-locked") as HTMLElement;
|
| 264 |
+
const proOnlyLabel = document.querySelector(".pro-only-label") as HTMLElement;
|
| 265 |
+
const proBadgeWrapper = document.querySelector(".pro-badge-wrapper") as HTMLElement;
|
| 266 |
+
|
| 267 |
+
// Hide loading state
|
| 268 |
+
if (loadingEl) loadingEl.style.display = "none";
|
| 269 |
+
|
| 270 |
+
// If PDF Pro gating is disabled, just show the download button
|
| 271 |
+
if (!pdfProOnly) {
|
| 272 |
+
if (proOnlyEl) proOnlyEl.style.display = "block";
|
| 273 |
+
if (proOnlyLabel) proOnlyLabel.style.display = "none";
|
| 274 |
+
if (lockedEl) lockedEl.style.display = "none";
|
| 275 |
+
if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
|
| 276 |
+
return;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Show appropriate state based on Pro status
|
| 280 |
+
if (isPro) {
|
| 281 |
+
if (proOnlyEl) proOnlyEl.style.display = "block";
|
| 282 |
+
if (proOnlyLabel) proOnlyLabel.style.display = "none";
|
| 283 |
+
if (lockedEl) lockedEl.style.display = "none";
|
| 284 |
+
if (proBadgeWrapper) proBadgeWrapper.style.display = "inline-flex";
|
| 285 |
+
} else {
|
| 286 |
+
if (proOnlyEl) proOnlyEl.style.display = "none";
|
| 287 |
+
if (proOnlyLabel) proOnlyLabel.style.display = "inline-flex";
|
| 288 |
+
if (lockedEl) lockedEl.style.display = "block";
|
| 289 |
+
if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/**
|
| 294 |
+
* Handle user plan response
|
| 295 |
+
*/
|
| 296 |
+
function handleUserPlan(plan: any) {
|
| 297 |
+
userPlanChecked = true;
|
| 298 |
+
const isPro = isProUser(plan);
|
| 299 |
+
updatePdfAccess(isPro);
|
| 300 |
+
|
| 301 |
+
// Optional: log for debugging
|
| 302 |
+
console.log("[PDF Access]", { plan, isPro });
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
/**
|
| 306 |
+
* Fallback behavior when no parent window responds
|
| 307 |
+
* Uses LOCAL_IS_PRO configuration for local development
|
| 308 |
+
*/
|
| 309 |
+
function handleFallback() {
|
| 310 |
+
if (LOCAL_IS_PRO) {
|
| 311 |
+
handleUserPlan({ user: "pro" });
|
| 312 |
+
} else {
|
| 313 |
+
handleUserPlan({ user: "free" });
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// If PDF Pro gating is disabled, show the download button immediately
|
| 318 |
+
if (!pdfProOnly) {
|
| 319 |
+
updatePdfAccess(true);
|
| 320 |
+
} else {
|
| 321 |
+
// Listen for messages from parent window (Hugging Face Spaces)
|
| 322 |
+
window.addEventListener("message", (event) => {
|
| 323 |
+
if (event.data.type === "USER_PLAN") {
|
| 324 |
+
handleUserPlan(event.data.plan);
|
| 325 |
+
}
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
+
// Request user plan on page load
|
| 329 |
+
if (window.parent && window.parent !== window) {
|
| 330 |
+
// We're in an iframe, request user plan
|
| 331 |
+
window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
|
| 332 |
+
|
| 333 |
+
// Fallback if no response after timeout
|
| 334 |
+
setTimeout(() => {
|
| 335 |
+
if (!userPlanChecked) {
|
| 336 |
+
handleFallback();
|
| 337 |
+
}
|
| 338 |
+
}, FALLBACK_TIMEOUT_MS);
|
| 339 |
+
} else {
|
| 340 |
+
// Not in iframe (local development), use fallback immediately
|
| 341 |
+
handleFallback();
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
</script>
|
| 345 |
+
|
| 346 |
<style>
|
| 347 |
/* Hero (full-width) */
|
| 348 |
.hero {
|
| 349 |
width: 100%;
|
| 350 |
+
padding: 48px 16px 16px;
|
| 351 |
text-align: center;
|
| 352 |
}
|
| 353 |
.hero-title {
|
|
|
|
| 404 |
display: flex;
|
| 405 |
flex-direction: column;
|
| 406 |
gap: 8px;
|
| 407 |
+
max-width: 250px;
|
| 408 |
}
|
| 409 |
.meta-container-cell h3 {
|
| 410 |
margin: 0;
|
|
|
|
| 421 |
margin: 0;
|
| 422 |
list-style-type: none;
|
| 423 |
padding-left: 0;
|
| 424 |
+
display: flex;
|
| 425 |
+
flex-wrap: wrap;
|
| 426 |
+
}
|
| 427 |
+
.authors li {
|
| 428 |
+
white-space: nowrap;
|
| 429 |
+
padding:0;
|
| 430 |
}
|
| 431 |
.affiliations {
|
| 432 |
margin: 0;
|
|
|
|
| 441 |
row-gap: 12px;
|
| 442 |
}
|
| 443 |
|
| 444 |
+
@media (max-width: 768px) {
|
| 445 |
+
.meta-container-cell--affiliations,
|
| 446 |
+
.meta-container-cell--pdf {
|
| 447 |
+
text-align: right;
|
| 448 |
+
}
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
@media print {
|
| 452 |
.meta-container-cell--pdf {
|
| 453 |
display: none !important;
|
| 454 |
}
|
| 455 |
}
|
| 456 |
+
|
| 457 |
+
/* PDF access control styles */
|
| 458 |
+
.pdf-header-wrapper {
|
| 459 |
+
display: flex;
|
| 460 |
+
align-items: center;
|
| 461 |
+
gap: 6px;
|
| 462 |
+
line-height: 1;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.pdf-header-wrapper h3 {
|
| 466 |
+
line-height: 1;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.pdf-loading {
|
| 470 |
+
color: var(--muted-color);
|
| 471 |
+
font-size: 0.9em;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.pdf-pro-only {
|
| 475 |
+
margin: 0;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.pro-badge-wrapper {
|
| 479 |
+
display: inline-flex;
|
| 480 |
+
align-items: center;
|
| 481 |
+
gap: 5px;
|
| 482 |
+
font-style: normal;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.pro-badge-prefix {
|
| 486 |
+
font-size: 0.85em;
|
| 487 |
+
opacity: 0.5;
|
| 488 |
+
font-weight: 400;
|
| 489 |
+
font-style: normal;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.pro-badge {
|
| 493 |
+
display: inline-block;
|
| 494 |
+
border: 1px solid rgba(0, 0, 0, 0.025);
|
| 495 |
+
background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047);
|
| 496 |
+
color: black;
|
| 497 |
+
padding: 1px 5px;
|
| 498 |
+
border-radius: 3px;
|
| 499 |
+
font-size: 0.5rem;
|
| 500 |
+
font-weight: 700;
|
| 501 |
+
font-style: normal;
|
| 502 |
+
letter-spacing: 0.025em;
|
| 503 |
+
text-transform: uppercase;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
/* Dark mode pro badge */
|
| 507 |
+
:global(.dark) .pro-badge,
|
| 508 |
+
:global([data-theme="dark"]) .pro-badge {
|
| 509 |
+
background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
|
| 510 |
+
border-color: rgba(255, 255, 255, 0.15);
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.pro-only-label {
|
| 514 |
+
display: inline-flex;
|
| 515 |
+
flex-direction: row;
|
| 516 |
+
align-items: center;
|
| 517 |
+
gap: 5px;
|
| 518 |
+
font-size: 0.85em;
|
| 519 |
+
opacity: 0.5;
|
| 520 |
+
font-weight: 400;
|
| 521 |
+
line-height: 1;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.pro-only-dash {
|
| 525 |
+
display: inline-flex;
|
| 526 |
+
align-items: center;
|
| 527 |
+
line-height: 1;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.pro-only-icon {
|
| 531 |
+
width: 11px;
|
| 532 |
+
height: 11px;
|
| 533 |
+
flex-shrink: 0;
|
| 534 |
+
display: inline-flex;
|
| 535 |
+
align-items: center;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.pro-only-text {
|
| 539 |
+
display: inline-flex;
|
| 540 |
+
align-items: center;
|
| 541 |
+
line-height: 1;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
.pdf-locked {
|
| 545 |
+
display: block;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.button-locked {
|
| 549 |
+
display: inline-flex;
|
| 550 |
+
align-items: center;
|
| 551 |
+
gap: 6px;
|
| 552 |
+
background: linear-gradient(135deg,
|
| 553 |
+
var(--primary-color) 0%,
|
| 554 |
+
oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);
|
| 555 |
+
border-radius: var(--button-radius);
|
| 556 |
+
padding: var(--button-padding-y) var(--button-padding-x);
|
| 557 |
+
font-size: var(--button-font-size);
|
| 558 |
+
line-height: 1;
|
| 559 |
+
color: var(--on-primary);
|
| 560 |
+
position: relative;
|
| 561 |
+
overflow: hidden;
|
| 562 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 563 |
+
font-weight: normal;
|
| 564 |
+
border-color: rgba(0, 0, 0, 0.15);
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.button-locked:active {
|
| 568 |
+
transform: translateY(0);
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.lock-icon {
|
| 572 |
+
font-size: 1em;
|
| 573 |
+
flex-shrink: 0;
|
| 574 |
+
position: relative;
|
| 575 |
+
z-index: 1;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.locked-title {
|
| 579 |
+
position: relative;
|
| 580 |
+
z-index: 1;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/* Dark mode locked button - inherits from light mode variables */
|
| 584 |
+
|
| 585 |
+
@media (max-width: 768px) {
|
| 586 |
+
.meta-container-cell--pdf {
|
| 587 |
+
display: flex;
|
| 588 |
+
flex-direction: column;
|
| 589 |
+
align-items: flex-end;
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
</style>
|
app/src/components/HtmlEmbed.astro
CHANGED
|
@@ -20,12 +20,15 @@ const html = resolveFragment(src);
|
|
| 20 |
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
| 21 |
const dataAttr = Array.isArray(data) ? JSON.stringify(data) : (typeof data === 'string' ? data : undefined);
|
| 22 |
const configAttr = typeof config === 'string' ? config : (config != null ? JSON.stringify(config) : undefined);
|
|
|
|
|
|
|
|
|
|
| 23 |
---
|
| 24 |
{ html ? (
|
| 25 |
<figure class="html-embed" id={id}>
|
| 26 |
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
|
| 27 |
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
| 28 |
-
<div id={mountId} data-datafiles={dataAttr} data-config={configAttr} set:html={
|
| 29 |
</div>
|
| 30 |
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
|
| 31 |
</figure>
|
|
@@ -70,7 +73,6 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON.
|
|
| 70 |
.html-embed { margin: 0 0 var(--block-spacing-y);
|
| 71 |
z-index: var(--z-elevated);
|
| 72 |
position: relative;
|
| 73 |
-
|
| 74 |
}
|
| 75 |
.html-embed__title {
|
| 76 |
text-align: left;
|
|
@@ -83,12 +85,14 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON.
|
|
| 83 |
position: relative;
|
| 84 |
display: block;
|
| 85 |
width: 100%;
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
.html-embed__card {
|
| 88 |
background: var(--code-bg);
|
| 89 |
border: 1px solid var(--border-color);
|
| 90 |
border-radius: 10px;
|
| 91 |
-
padding:
|
| 92 |
z-index: calc(var(--z-elevated) + 1);
|
| 93 |
position: relative;
|
| 94 |
}
|
|
@@ -108,6 +112,7 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON.
|
|
| 108 |
z-index: var(--z-elevated);
|
| 109 |
display: block;
|
| 110 |
width: 100%;
|
|
|
|
| 111 |
}
|
| 112 |
/* Plotly – fragments & controls */
|
| 113 |
.html-embed__card svg text { fill: var(--text-color); }
|
|
|
|
| 20 |
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
| 21 |
const dataAttr = Array.isArray(data) ? JSON.stringify(data) : (typeof data === 'string' ? data : undefined);
|
| 22 |
const configAttr = typeof config === 'string' ? config : (config != null ? JSON.stringify(config) : undefined);
|
| 23 |
+
|
| 24 |
+
// Apply the ID to the HTML content if provided
|
| 25 |
+
const htmlWithId = id && html ? html.replace(/<div class="([^"]*)"[^>]*>/, `<div class="$1" id="${id}">`) : html;
|
| 26 |
---
|
| 27 |
{ html ? (
|
| 28 |
<figure class="html-embed" id={id}>
|
| 29 |
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
|
| 30 |
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
| 31 |
+
<div id={mountId} data-datafiles={dataAttr} data-config={configAttr} set:html={htmlWithId} />
|
| 32 |
</div>
|
| 33 |
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
|
| 34 |
</figure>
|
|
|
|
| 73 |
.html-embed { margin: 0 0 var(--block-spacing-y);
|
| 74 |
z-index: var(--z-elevated);
|
| 75 |
position: relative;
|
|
|
|
| 76 |
}
|
| 77 |
.html-embed__title {
|
| 78 |
text-align: left;
|
|
|
|
| 85 |
position: relative;
|
| 86 |
display: block;
|
| 87 |
width: 100%;
|
| 88 |
+
background: var(--page-bg);
|
| 89 |
+
z-index: var(--z-elevated);
|
| 90 |
}
|
| 91 |
.html-embed__card {
|
| 92 |
background: var(--code-bg);
|
| 93 |
border: 1px solid var(--border-color);
|
| 94 |
border-radius: 10px;
|
| 95 |
+
padding: 24px;
|
| 96 |
z-index: calc(var(--z-elevated) + 1);
|
| 97 |
position: relative;
|
| 98 |
}
|
|
|
|
| 112 |
z-index: var(--z-elevated);
|
| 113 |
display: block;
|
| 114 |
width: 100%;
|
| 115 |
+
background: var(--page-bg);
|
| 116 |
}
|
| 117 |
/* Plotly – fragments & controls */
|
| 118 |
.html-embed__card svg text { fill: var(--text-color); }
|
app/src/components/Image.astro
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
// @ts-ignore - types provided by Astro at runtime
|
| 3 |
+
import { Image as AstroImage } from "astro:assets";
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
/** Source image imported via astro:assets */
|
| 7 |
+
src: any;
|
| 8 |
+
/** Alt text for accessibility */
|
| 9 |
+
alt: string;
|
| 10 |
+
/** Optional HTML string caption (use slot caption for rich content) */
|
| 11 |
+
caption?: string;
|
| 12 |
+
/** Optional class to apply on the <figure> wrapper when caption is used */
|
| 13 |
+
figureClass?: string;
|
| 14 |
+
/** Enable medium-zoom behavior on this image */
|
| 15 |
+
zoomable?: boolean;
|
| 16 |
+
/** Show a download button overlay and enable download flow */
|
| 17 |
+
downloadable?: boolean;
|
| 18 |
+
/** Optional explicit file name to use on download */
|
| 19 |
+
downloadName?: string;
|
| 20 |
+
/** Optional explicit source URL to download instead of currentSrc */
|
| 21 |
+
downloadSrc?: string;
|
| 22 |
+
/** Optional link that wraps the image (not the caption) */
|
| 23 |
+
linkHref?: string;
|
| 24 |
+
/** Optional target for the link (default: _blank when linkHref provided) */
|
| 25 |
+
linkTarget?: string;
|
| 26 |
+
/** Optional rel for the link (default: noopener noreferrer when linkHref provided) */
|
| 27 |
+
linkRel?: string;
|
| 28 |
+
/** Make the image span full width */
|
| 29 |
+
fullWidth?: boolean;
|
| 30 |
+
/** Any additional attributes should be forwarded to the underlying <AstroImage> */
|
| 31 |
+
[key: string]: any;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const {
|
| 35 |
+
caption,
|
| 36 |
+
figureClass,
|
| 37 |
+
zoomable,
|
| 38 |
+
downloadable,
|
| 39 |
+
downloadName,
|
| 40 |
+
downloadSrc,
|
| 41 |
+
linkHref,
|
| 42 |
+
linkTarget,
|
| 43 |
+
linkRel,
|
| 44 |
+
fullWidth,
|
| 45 |
+
...imgProps
|
| 46 |
+
} = Astro.props as Props;
|
| 47 |
+
const hasCaptionSlot = Astro.slots.has("caption");
|
| 48 |
+
const hasCaption =
|
| 49 |
+
hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
|
| 50 |
+
const hasTitle = Astro.slots.has("title");
|
| 51 |
+
const uid = `ri_${Math.random().toString(36).slice(2)}`;
|
| 52 |
+
const dataZoomable =
|
| 53 |
+
zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined;
|
| 54 |
+
const dataDownloadable =
|
| 55 |
+
downloadable === true || (imgProps as any)["data-downloadable"]
|
| 56 |
+
? "1"
|
| 57 |
+
: undefined;
|
| 58 |
+
const hasLink = typeof linkHref === "string" && linkHref.length > 0;
|
| 59 |
+
const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
|
| 60 |
+
const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
<div
|
| 64 |
+
class={`ri-root`}
|
| 65 |
+
data-ri-root={uid}
|
| 66 |
+
data-has-title={hasTitle}
|
| 67 |
+
data-has-caption={hasCaption}
|
| 68 |
+
>
|
| 69 |
+
{
|
| 70 |
+
hasCaption ? (
|
| 71 |
+
<figure
|
| 72 |
+
class={(figureClass || "") + (dataDownloadable ? " has-dl-btn" : "")}
|
| 73 |
+
>
|
| 74 |
+
{dataDownloadable ? (
|
| 75 |
+
<span class="img-dl-wrap">
|
| 76 |
+
{hasLink ? (
|
| 77 |
+
<a
|
| 78 |
+
class="ri-link"
|
| 79 |
+
href={linkHref}
|
| 80 |
+
target={resolvedTarget}
|
| 81 |
+
rel={resolvedRel}
|
| 82 |
+
>
|
| 83 |
+
<AstroImage
|
| 84 |
+
{...imgProps}
|
| 85 |
+
data-zoomable={dataZoomable}
|
| 86 |
+
data-downloadable={dataDownloadable}
|
| 87 |
+
data-download-name={downloadName}
|
| 88 |
+
data-download-src={downloadSrc}
|
| 89 |
+
/>
|
| 90 |
+
</a>
|
| 91 |
+
) : (
|
| 92 |
+
<AstroImage
|
| 93 |
+
{...imgProps}
|
| 94 |
+
data-zoomable={dataZoomable}
|
| 95 |
+
data-downloadable={dataDownloadable}
|
| 96 |
+
data-download-name={downloadName}
|
| 97 |
+
data-download-src={downloadSrc}
|
| 98 |
+
/>
|
| 99 |
+
)}
|
| 100 |
+
<button
|
| 101 |
+
type="button"
|
| 102 |
+
class="button img-dl-btn"
|
| 103 |
+
aria-label="Download image"
|
| 104 |
+
title={
|
| 105 |
+
downloadName ? `Download ${downloadName}` : "Download image"
|
| 106 |
+
}
|
| 107 |
+
>
|
| 108 |
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
| 109 |
+
<path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z" />
|
| 110 |
+
</svg>
|
| 111 |
+
</button>
|
| 112 |
+
</span>
|
| 113 |
+
) : hasLink ? (
|
| 114 |
+
<a
|
| 115 |
+
class="ri-link"
|
| 116 |
+
href={linkHref}
|
| 117 |
+
target={resolvedTarget}
|
| 118 |
+
rel={resolvedRel}
|
| 119 |
+
>
|
| 120 |
+
<AstroImage {...imgProps} data-zoomable={dataZoomable} />
|
| 121 |
+
</a>
|
| 122 |
+
) : (
|
| 123 |
+
<AstroImage {...imgProps} data-zoomable={dataZoomable} />
|
| 124 |
+
)}
|
| 125 |
+
<figcaption>
|
| 126 |
+
{hasCaptionSlot ? (
|
| 127 |
+
<slot name="caption" />
|
| 128 |
+
) : (
|
| 129 |
+
caption && <span set:html={caption} />
|
| 130 |
+
)}
|
| 131 |
+
</figcaption>
|
| 132 |
+
</figure>
|
| 133 |
+
) : dataDownloadable ? (
|
| 134 |
+
<span class="img-dl-wrap">
|
| 135 |
+
{hasLink ? (
|
| 136 |
+
<a
|
| 137 |
+
class="ri-link"
|
| 138 |
+
href={linkHref}
|
| 139 |
+
target={resolvedTarget}
|
| 140 |
+
rel={resolvedRel}
|
| 141 |
+
>
|
| 142 |
+
<AstroImage
|
| 143 |
+
{...imgProps}
|
| 144 |
+
data-zoomable={dataZoomable}
|
| 145 |
+
data-downloadable={dataDownloadable}
|
| 146 |
+
data-download-name={downloadName}
|
| 147 |
+
data-download-src={downloadSrc}
|
| 148 |
+
/>
|
| 149 |
+
</a>
|
| 150 |
+
) : (
|
| 151 |
+
<AstroImage
|
| 152 |
+
{...imgProps}
|
| 153 |
+
data-zoomable={dataZoomable}
|
| 154 |
+
data-downloadable={dataDownloadable}
|
| 155 |
+
data-download-name={downloadName}
|
| 156 |
+
data-download-src={downloadSrc}
|
| 157 |
+
/>
|
| 158 |
+
)}
|
| 159 |
+
<button
|
| 160 |
+
type="button"
|
| 161 |
+
class="button img-dl-btn"
|
| 162 |
+
aria-label="Download image"
|
| 163 |
+
title={downloadName ? `Download ${downloadName}` : "Download image"}
|
| 164 |
+
>
|
| 165 |
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
| 166 |
+
<path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z" />
|
| 167 |
+
</svg>
|
| 168 |
+
</button>
|
| 169 |
+
</span>
|
| 170 |
+
) : hasLink ? (
|
| 171 |
+
<a
|
| 172 |
+
class="ri-link"
|
| 173 |
+
href={linkHref}
|
| 174 |
+
target={resolvedTarget}
|
| 175 |
+
rel={resolvedRel}
|
| 176 |
+
>
|
| 177 |
+
<AstroImage
|
| 178 |
+
{...imgProps}
|
| 179 |
+
data-zoomable={dataZoomable}
|
| 180 |
+
class={fullWidth ? "full" : ""}
|
| 181 |
+
/>
|
| 182 |
+
</a>
|
| 183 |
+
) : (
|
| 184 |
+
<AstroImage
|
| 185 |
+
{...imgProps}
|
| 186 |
+
data-zoomable={dataZoomable}
|
| 187 |
+
class={fullWidth ? "full" : ""}
|
| 188 |
+
/>
|
| 189 |
+
)
|
| 190 |
+
}
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<script is:inline>
|
| 194 |
+
(() => {
|
| 195 |
+
const scriptEl = document.currentScript;
|
| 196 |
+
const root = scriptEl ? scriptEl.previousElementSibling : null;
|
| 197 |
+
if (!root) {
|
| 198 |
+
console.log("Figure script: No root element found, exiting");
|
| 199 |
+
return;
|
| 200 |
+
}
|
| 201 |
+
const img =
|
| 202 |
+
root.tagName === "IMG"
|
| 203 |
+
? root
|
| 204 |
+
: root.querySelector
|
| 205 |
+
? root.querySelector("img")
|
| 206 |
+
: null;
|
| 207 |
+
if (!img) {
|
| 208 |
+
console.log("Figure script: No img element found, exiting");
|
| 209 |
+
return;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// medium-zoom integration scoped to this image only
|
| 213 |
+
const ensureMediumZoomReady = (cb) => {
|
| 214 |
+
// @ts-ignore
|
| 215 |
+
if (window.mediumZoom) return cb();
|
| 216 |
+
const retry = () => {
|
| 217 |
+
// @ts-ignore
|
| 218 |
+
if (window.mediumZoom) cb();
|
| 219 |
+
else setTimeout(retry, 30);
|
| 220 |
+
};
|
| 221 |
+
retry();
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
const initZoomIfNeeded = () => {
|
| 225 |
+
if (img.getAttribute("data-zoomable") !== "1") return;
|
| 226 |
+
const isDark =
|
| 227 |
+
document.documentElement.getAttribute("data-theme") === "dark";
|
| 228 |
+
const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";
|
| 229 |
+
ensureMediumZoomReady(() => {
|
| 230 |
+
// @ts-ignore
|
| 231 |
+
const instance = window.mediumZoom
|
| 232 |
+
? window.mediumZoom(img, { background, margin: 24, scrollOffset: 0 })
|
| 233 |
+
: null;
|
| 234 |
+
if (!instance) return;
|
| 235 |
+
let onScrollLike;
|
| 236 |
+
const attachCloseOnScroll = () => {
|
| 237 |
+
if (onScrollLike) return;
|
| 238 |
+
onScrollLike = () => {
|
| 239 |
+
try {
|
| 240 |
+
instance.close && instance.close();
|
| 241 |
+
} catch {}
|
| 242 |
+
};
|
| 243 |
+
window.addEventListener("wheel", onScrollLike, { passive: true });
|
| 244 |
+
window.addEventListener("touchmove", onScrollLike, { passive: true });
|
| 245 |
+
window.addEventListener("scroll", onScrollLike, { passive: true });
|
| 246 |
+
};
|
| 247 |
+
const detachCloseOnScroll = () => {
|
| 248 |
+
if (!onScrollLike) return;
|
| 249 |
+
window.removeEventListener("wheel", onScrollLike);
|
| 250 |
+
window.removeEventListener("touchmove", onScrollLike);
|
| 251 |
+
window.removeEventListener("scroll", onScrollLike);
|
| 252 |
+
onScrollLike = null;
|
| 253 |
+
};
|
| 254 |
+
try {
|
| 255 |
+
instance.on && instance.on("open", attachCloseOnScroll);
|
| 256 |
+
} catch {}
|
| 257 |
+
try {
|
| 258 |
+
instance.on && instance.on("close", detachCloseOnScroll);
|
| 259 |
+
} catch {}
|
| 260 |
+
const themeObserver = new MutationObserver(() => {
|
| 261 |
+
const dark =
|
| 262 |
+
document.documentElement.getAttribute("data-theme") === "dark";
|
| 263 |
+
try {
|
| 264 |
+
instance.update &&
|
| 265 |
+
instance.update({
|
| 266 |
+
background: dark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)",
|
| 267 |
+
});
|
| 268 |
+
} catch {}
|
| 269 |
+
});
|
| 270 |
+
themeObserver.observe(document.documentElement, {
|
| 271 |
+
attributes: true,
|
| 272 |
+
attributeFilter: ["data-theme"],
|
| 273 |
+
});
|
| 274 |
+
});
|
| 275 |
+
};
|
| 276 |
+
|
| 277 |
+
// Global zoom management to hide other Figures
|
| 278 |
+
const setupGlobalZoomBehavior = () => {
|
| 279 |
+
img.addEventListener("click", () => {
|
| 280 |
+
if (img.getAttribute("data-zoomable") === "1") {
|
| 281 |
+
// Enlever zoom-active de tous les autres ri-root
|
| 282 |
+
document
|
| 283 |
+
.querySelectorAll(".ri-root.zoom-active")
|
| 284 |
+
.forEach((el) => el.classList.remove("zoom-active"));
|
| 285 |
+
|
| 286 |
+
// Add zoom-active to this ri-root
|
| 287 |
+
root.classList.add("zoom-active");
|
| 288 |
+
}
|
| 289 |
+
});
|
| 290 |
+
};
|
| 291 |
+
|
| 292 |
+
// Download button handler
|
| 293 |
+
const dlBtn = root.querySelector ? root.querySelector(".img-dl-btn") : null;
|
| 294 |
+
if (dlBtn) {
|
| 295 |
+
dlBtn.addEventListener("click", async (ev) => {
|
| 296 |
+
try {
|
| 297 |
+
ev.preventDefault();
|
| 298 |
+
ev.stopPropagation();
|
| 299 |
+
const pickHrefAndName = () => {
|
| 300 |
+
const current = img.currentSrc || img.src || "";
|
| 301 |
+
let href = img.getAttribute("data-download-src") || current;
|
| 302 |
+
const deriveName = () => {
|
| 303 |
+
try {
|
| 304 |
+
const u = new URL(current, location.href);
|
| 305 |
+
const rawHref = u.searchParams.get("href");
|
| 306 |
+
const candidate = rawHref
|
| 307 |
+
? decodeURIComponent(rawHref)
|
| 308 |
+
: u.pathname;
|
| 309 |
+
const last = String(candidate).split("/").pop() || "";
|
| 310 |
+
const base = last.split("?")[0].split("#")[0];
|
| 311 |
+
const m = base.match(
|
| 312 |
+
/^(.+?\.(?:png|jpe?g|webp|avif|gif|svg))(?:[._-].*)?$/i,
|
| 313 |
+
);
|
| 314 |
+
if (m && m[1]) return m[1];
|
| 315 |
+
return base || "image";
|
| 316 |
+
} catch {
|
| 317 |
+
return "image";
|
| 318 |
+
}
|
| 319 |
+
};
|
| 320 |
+
const name = img.getAttribute("data-download-name") || deriveName();
|
| 321 |
+
return { href, name };
|
| 322 |
+
};
|
| 323 |
+
const picked = pickHrefAndName();
|
| 324 |
+
const res = await fetch(picked.href, { credentials: "same-origin" });
|
| 325 |
+
const blob = await res.blob();
|
| 326 |
+
const objectUrl = URL.createObjectURL(blob);
|
| 327 |
+
const tmp = document.createElement("a");
|
| 328 |
+
tmp.href = objectUrl;
|
| 329 |
+
tmp.download = picked.name || "image";
|
| 330 |
+
tmp.target = "_self";
|
| 331 |
+
tmp.rel = "noopener";
|
| 332 |
+
tmp.style.display = "none";
|
| 333 |
+
document.body.appendChild(tmp);
|
| 334 |
+
tmp.click();
|
| 335 |
+
setTimeout(() => {
|
| 336 |
+
URL.revokeObjectURL(objectUrl);
|
| 337 |
+
tmp.remove();
|
| 338 |
+
}, 1000);
|
| 339 |
+
} catch {}
|
| 340 |
+
});
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// Setup comportement zoom
|
| 344 |
+
setupGlobalZoomBehavior();
|
| 345 |
+
|
| 346 |
+
if (document.readyState === "complete") initZoomIfNeeded();
|
| 347 |
+
else window.addEventListener("load", initZoomIfNeeded, { once: true });
|
| 348 |
+
})();
|
| 349 |
+
</script>
|
| 350 |
+
|
| 351 |
+
<style>
|
| 352 |
+
figure {
|
| 353 |
+
margin: var(--block-spacing-y) 0;
|
| 354 |
+
}
|
| 355 |
+
figcaption {
|
| 356 |
+
text-align: left;
|
| 357 |
+
font-size: 0.9rem;
|
| 358 |
+
color: var(--muted-color);
|
| 359 |
+
margin-top: 6px;
|
| 360 |
+
}
|
| 361 |
+
figcaption {
|
| 362 |
+
background: var(--page-bg);
|
| 363 |
+
position: relative;
|
| 364 |
+
z-index: var(--z-elevated);
|
| 365 |
+
display: block;
|
| 366 |
+
width: 100%;
|
| 367 |
+
}
|
| 368 |
+
.image-credit {
|
| 369 |
+
display: block;
|
| 370 |
+
margin-top: 4px;
|
| 371 |
+
font-size: 12px;
|
| 372 |
+
color: var(--muted-color);
|
| 373 |
+
}
|
| 374 |
+
.image-credit a {
|
| 375 |
+
color: inherit;
|
| 376 |
+
text-decoration: underline;
|
| 377 |
+
text-underline-offset: 2px;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
/* Zoomable overlay container (if used by any lightbox implementation) */
|
| 381 |
+
[data-zoom-overlay],
|
| 382 |
+
.zoom-overlay {
|
| 383 |
+
position: fixed;
|
| 384 |
+
inset: 0;
|
| 385 |
+
z-index: var(--z-overlay);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
/* Download link inside figures */
|
| 389 |
+
figure .download-link {
|
| 390 |
+
position: relative;
|
| 391 |
+
z-index: var(--z-elevated);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/* Opt-in zoomable images */
|
| 395 |
+
img[data-zoomable] {
|
| 396 |
+
cursor: zoom-in;
|
| 397 |
+
}
|
| 398 |
+
.medium-zoom--opened img[data-zoomable] {
|
| 399 |
+
cursor: zoom-out;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
/* Download button for img[data-downloadable] */
|
| 403 |
+
figure.has-dl-btn {
|
| 404 |
+
position: relative;
|
| 405 |
+
}
|
| 406 |
+
.dl-host {
|
| 407 |
+
position: relative;
|
| 408 |
+
}
|
| 409 |
+
.img-dl-wrap {
|
| 410 |
+
position: relative;
|
| 411 |
+
display: inline-block;
|
| 412 |
+
}
|
| 413 |
+
.img-dl-btn {
|
| 414 |
+
position: absolute;
|
| 415 |
+
right: 8px;
|
| 416 |
+
bottom: 8px;
|
| 417 |
+
align-items: center;
|
| 418 |
+
justify-content: center;
|
| 419 |
+
width: 30px;
|
| 420 |
+
height: 30px;
|
| 421 |
+
border-radius: 6px;
|
| 422 |
+
color: white;
|
| 423 |
+
text-decoration: none;
|
| 424 |
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
| 425 |
+
z-index: var(--z-elevated);
|
| 426 |
+
display: none;
|
| 427 |
+
background: var(--primary-color);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/* When an image is zoomed, hide ALL Figures on the page */
|
| 431 |
+
:global(.medium-zoom--opened) .ri-root {
|
| 432 |
+
opacity: 0;
|
| 433 |
+
z-index: calc(var(--z-base) - 1);
|
| 434 |
+
transition: opacity 0.3s ease;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
/* The currently zoomed image remains visible */
|
| 438 |
+
:global(.medium-zoom--opened) .ri-root:has(.medium-zoom--opened) {
|
| 439 |
+
opacity: 1;
|
| 440 |
+
z-index: var(--z-overlay);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* Fallback for browsers without :has() support */
|
| 444 |
+
:global(.medium-zoom--opened) .ri-root.zoom-active {
|
| 445 |
+
opacity: 1 !important;
|
| 446 |
+
z-index: var(--z-overlay) !important;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
/* Specifically hide download button and figcaption during zoom */
|
| 450 |
+
:global(.medium-zoom--opened) .img-dl-btn {
|
| 451 |
+
opacity: 0;
|
| 452 |
+
z-index: calc(var(--z-base) - 1);
|
| 453 |
+
transition: opacity 0.3s ease;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
:global(.medium-zoom--opened) figcaption {
|
| 457 |
+
opacity: 0;
|
| 458 |
+
z-index: calc(var(--z-base) - 1);
|
| 459 |
+
transition: opacity 0.3s ease;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
/* Even for active zoomed image, hide button and caption for clean experience */
|
| 463 |
+
:global(.medium-zoom--opened) .ri-root.zoom-active .img-dl-btn {
|
| 464 |
+
opacity: 0;
|
| 465 |
+
z-index: calc(var(--z-base) - 1);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
:global(.medium-zoom--opened) .ri-root.zoom-active figcaption {
|
| 469 |
+
opacity: 0;
|
| 470 |
+
z-index: calc(var(--z-base) - 1);
|
| 471 |
+
}
|
| 472 |
+
.img-dl-btn svg {
|
| 473 |
+
width: 18px;
|
| 474 |
+
height: 18px;
|
| 475 |
+
fill: currentColor;
|
| 476 |
+
}
|
| 477 |
+
.img-dl-wrap:hover .img-dl-btn {
|
| 478 |
+
display: inline-flex;
|
| 479 |
+
}
|
| 480 |
+
.img-dl-btn:hover {
|
| 481 |
+
background: var(--primary-color-hover);
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
[data-theme="dark"] .img-dl-btn {
|
| 485 |
+
background: var(--primary-color);
|
| 486 |
+
color: var(--on-primary);
|
| 487 |
+
border-color: var(--primary-color);
|
| 488 |
+
}
|
| 489 |
+
[data-theme="dark"] .img-dl-btn:hover {
|
| 490 |
+
background: var(--primary-color-hover);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
/* Conditional margins based on title and caption presence */
|
| 494 |
+
.ri-root:not([data-has-title="true"]) {
|
| 495 |
+
margin-top: 20px;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.ri-root:not([data-has-caption="true"]) {
|
| 499 |
+
margin-bottom: 20px;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
/* full image styles */
|
| 503 |
+
img.full {
|
| 504 |
+
width: 100% !important;
|
| 505 |
+
min-width: 100%;
|
| 506 |
+
max-width: 100%;
|
| 507 |
+
}
|
| 508 |
+
</style>
|
app/src/components/Quote.astro
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
interface Props {
|
| 3 |
+
source?: string;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
const { source } = Astro.props;
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
<blockquote class="quote">
|
| 10 |
+
<div class="quote__text">
|
| 11 |
+
<slot />
|
| 12 |
+
</div>
|
| 13 |
+
{
|
| 14 |
+
source && (
|
| 15 |
+
<footer class="quote__footer">
|
| 16 |
+
<span class="quote__source" set:html={source} />
|
| 17 |
+
</footer>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
</blockquote>
|
| 21 |
+
|
| 22 |
+
<style>
|
| 23 |
+
.quote {
|
| 24 |
+
font-family: var(--default-font-family);
|
| 25 |
+
margin: 32px 0;
|
| 26 |
+
max-width: 600px;
|
| 27 |
+
text-align: left;
|
| 28 |
+
position: relative;
|
| 29 |
+
padding: 0;
|
| 30 |
+
border: none;
|
| 31 |
+
background: none;
|
| 32 |
+
box-shadow: none;
|
| 33 |
+
white-space: normal;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.quote::before {
|
| 37 |
+
content: '"';
|
| 38 |
+
position: absolute;
|
| 39 |
+
top: -24px;
|
| 40 |
+
left: -30px;
|
| 41 |
+
font-size: 8rem;
|
| 42 |
+
font-family: var(--default-font-family);
|
| 43 |
+
font-weight: 400;
|
| 44 |
+
color: var(--text-color);
|
| 45 |
+
opacity: 0.05;
|
| 46 |
+
z-index: -1;
|
| 47 |
+
line-height: 1;
|
| 48 |
+
pointer-events: none;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.quote__text {
|
| 52 |
+
font-size: 1.5rem;
|
| 53 |
+
line-height: 1.4;
|
| 54 |
+
font-style: normal;
|
| 55 |
+
font-weight: 400;
|
| 56 |
+
font-family:
|
| 57 |
+
"SF Pro Text",
|
| 58 |
+
-apple-system,
|
| 59 |
+
BlinkMacSystemFont,
|
| 60 |
+
"Segoe UI",
|
| 61 |
+
sans-serif;
|
| 62 |
+
color: var(--text-color);
|
| 63 |
+
margin-bottom: 12px;
|
| 64 |
+
padding: 0;
|
| 65 |
+
letter-spacing: -0.01em;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.quote__text p {
|
| 69 |
+
margin: 0;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.quote__footer {
|
| 73 |
+
font-size: 0.875rem;
|
| 74 |
+
color: var(--muted-color);
|
| 75 |
+
display: flex;
|
| 76 |
+
justify-content: flex-start;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 6px;
|
| 79 |
+
margin-top: 0;
|
| 80 |
+
font-family:
|
| 81 |
+
"SF Pro Text",
|
| 82 |
+
-apple-system,
|
| 83 |
+
BlinkMacSystemFont,
|
| 84 |
+
"Segoe UI",
|
| 85 |
+
sans-serif;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.quote__source {
|
| 89 |
+
font-weight: 500;
|
| 90 |
+
opacity: 0.85;
|
| 91 |
+
font-style: italic;
|
| 92 |
+
color: var(--text-color);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.quote__source::before {
|
| 96 |
+
content: "— ";
|
| 97 |
+
font-style: normal;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
@media (max-width: 640px) {
|
| 101 |
+
.quote {
|
| 102 |
+
margin: 24px 0;
|
| 103 |
+
max-width: 100%;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.quote::before {
|
| 107 |
+
font-size: 6rem;
|
| 108 |
+
top: -18px;
|
| 109 |
+
left: -16px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.quote__text {
|
| 113 |
+
font-size: 1.25rem;
|
| 114 |
+
line-height: 1.45;
|
| 115 |
+
padding: 0;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.quote__footer {
|
| 119 |
+
flex-direction: row;
|
| 120 |
+
gap: 6px;
|
| 121 |
+
font-size: 0.8rem;
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
</style>
|
app/src/components/Reference.astro
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
interface Props {
|
| 3 |
+
/** ID unique pour la référence */
|
| 4 |
+
id: string;
|
| 5 |
+
/** Légende HTML pour la référence */
|
| 6 |
+
caption: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
const { id, caption } = Astro.props as Props;
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
<div class="reference-wrapper" id={id}>
|
| 13 |
+
<figure class="reference">
|
| 14 |
+
<div class="reference__content">
|
| 15 |
+
<slot />
|
| 16 |
+
</div>
|
| 17 |
+
<figcaption class="reference__caption" set:html={caption} />
|
| 18 |
+
</figure>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<style>
|
| 22 |
+
.reference-wrapper {
|
| 23 |
+
margin: var(--block-spacing-y) 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.reference {
|
| 27 |
+
margin: 0;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.reference__content {
|
| 31 |
+
/* Le contenu peut être n'importe quoi */
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.reference__caption {
|
| 35 |
+
text-align: left;
|
| 36 |
+
font-size: 0.9rem;
|
| 37 |
+
color: var(--muted-color);
|
| 38 |
+
margin-top: 6px;
|
| 39 |
+
background: var(--page-bg);
|
| 40 |
+
position: relative;
|
| 41 |
+
z-index: var(--z-elevated);
|
| 42 |
+
display: block;
|
| 43 |
+
width: 100%;
|
| 44 |
+
}
|
| 45 |
+
</style>
|
app/src/components/Sidenote.astro
CHANGED
|
@@ -1,37 +1,99 @@
|
|
| 1 |
---
|
|
|
|
| 2 |
---
|
| 3 |
-
|
| 4 |
-
|
|
|
|
| 5 |
<slot />
|
| 6 |
-
</div>
|
| 7 |
-
<aside class="aside__aside">
|
| 8 |
-
<slot name="aside" />
|
| 9 |
</aside>
|
| 10 |
</div>
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
<style is:global>
|
| 14 |
-
.
|
|
|
|
| 15 |
position: relative;
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
-
.
|
| 20 |
-
position: absolute;
|
| 21 |
-
top: 0;
|
| 22 |
-
right: -260px; /* push into the right grid column (width 260 + gap 32) */
|
| 23 |
-
width: 260px;
|
| 24 |
border-radius: 8px;
|
| 25 |
padding: 0 30px;
|
| 26 |
font-size: 0.9rem;
|
| 27 |
color: var(--muted-color);
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
@media (--bp-content-collapse) {
|
| 31 |
-
.
|
| 32 |
-
position
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
margin-top: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
}
|
| 37 |
</style>
|
|
|
|
| 1 |
---
|
| 2 |
+
|
| 3 |
---
|
| 4 |
+
|
| 5 |
+
<div class="sidenote-container">
|
| 6 |
+
<aside class="sidenote">
|
| 7 |
<slot />
|
|
|
|
|
|
|
|
|
|
| 8 |
</aside>
|
| 9 |
</div>
|
| 10 |
|
| 11 |
+
<script>
|
| 12 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 13 |
+
const containers = document.querySelectorAll(".sidenote-container");
|
| 14 |
+
|
| 15 |
+
containers.forEach((container) => {
|
| 16 |
+
// Find the previous element (sibling just before)
|
| 17 |
+
const previousElement = container.previousElementSibling as HTMLElement;
|
| 18 |
+
|
| 19 |
+
if (previousElement && previousElement.parentNode) {
|
| 20 |
+
// Create a wrapper div that will contain both the previous element and the sidenote
|
| 21 |
+
const wrapper = document.createElement("div");
|
| 22 |
+
wrapper.className = "sidenote-wrapper";
|
| 23 |
+
|
| 24 |
+
// Insert the wrapper before the previous element
|
| 25 |
+
previousElement.parentNode.insertBefore(wrapper, previousElement);
|
| 26 |
+
|
| 27 |
+
// Move both the previous element and the sidenote container into the wrapper
|
| 28 |
+
wrapper.appendChild(previousElement);
|
| 29 |
+
wrapper.appendChild(container);
|
| 30 |
+
|
| 31 |
+
// Style the wrapper to create the layout
|
| 32 |
+
wrapper.style.position = "relative";
|
| 33 |
+
wrapper.style.display = "block";
|
| 34 |
+
|
| 35 |
+
// Style the sidenote container so it positions correctly
|
| 36 |
+
const sidenoteContainer = container as HTMLElement;
|
| 37 |
+
sidenoteContainer.style.position = "absolute";
|
| 38 |
+
sidenoteContainer.style.top = "0";
|
| 39 |
+
sidenoteContainer.style.right = "-292px"; // 260px width + 32px gap
|
| 40 |
+
sidenoteContainer.style.width = "260px";
|
| 41 |
+
|
| 42 |
+
// Display the container with a fade-in
|
| 43 |
+
sidenoteContainer.style.display = "block";
|
| 44 |
+
sidenoteContainer.style.opacity = "0";
|
| 45 |
+
|
| 46 |
+
// Fade-in with transition
|
| 47 |
+
setTimeout(() => {
|
| 48 |
+
sidenoteContainer.style.opacity = "1";
|
| 49 |
+
}, 10);
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
});
|
| 53 |
+
</script>
|
| 54 |
|
| 55 |
<style is:global>
|
| 56 |
+
.sidenote-wrapper {
|
| 57 |
+
/* Le wrapper contient l'élément original et le sidenote */
|
| 58 |
position: relative;
|
| 59 |
+
display: block;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.sidenote-container {
|
| 63 |
+
/* Caché par défaut, sera affiché par JS */
|
| 64 |
+
display: none;
|
| 65 |
+
margin: 0;
|
| 66 |
+
/* Transition for fade-in */
|
| 67 |
+
transition: opacity 0.3s ease-in-out;
|
| 68 |
}
|
| 69 |
|
| 70 |
+
.sidenote {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
border-radius: 8px;
|
| 72 |
padding: 0 30px;
|
| 73 |
font-size: 0.9rem;
|
| 74 |
color: var(--muted-color);
|
| 75 |
+
margin: 0;
|
| 76 |
}
|
| 77 |
|
| 78 |
@media (--bp-content-collapse) {
|
| 79 |
+
.sidenote-wrapper {
|
| 80 |
+
/* Sur mobile, le wrapper n'a pas besoin de position relative */
|
| 81 |
+
position: static !important;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.sidenote-container {
|
| 85 |
+
position: static !important;
|
| 86 |
+
width: auto !important;
|
| 87 |
+
right: auto !important;
|
| 88 |
+
top: auto !important;
|
| 89 |
margin-top: 8px;
|
| 90 |
+
/* Affichage normal sur mobile */
|
| 91 |
+
display: block !important;
|
| 92 |
+
opacity: 1 !important;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.sidenote {
|
| 96 |
+
padding: 0;
|
| 97 |
}
|
| 98 |
}
|
| 99 |
</style>
|
app/src/components/Stack.astro
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
interface Props {
|
| 3 |
+
/** Layout mode: number of columns or 'auto' for responsive */
|
| 4 |
+
layout?: "2-column" | "3-column" | "4-column" | "auto";
|
| 5 |
+
/** Gap between items - can be a predefined size or custom value (e.g., "2rem", "20px", "1.5em") */
|
| 6 |
+
gap?: "small" | "medium" | "large" | string;
|
| 7 |
+
/** Optional class to apply on the wrapper */
|
| 8 |
+
class?: string;
|
| 9 |
+
/** Optional ID for the stack */
|
| 10 |
+
id?: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const {
|
| 14 |
+
layout = "2-column",
|
| 15 |
+
gap = "medium",
|
| 16 |
+
class: className,
|
| 17 |
+
id,
|
| 18 |
+
} = Astro.props as Props;
|
| 19 |
+
|
| 20 |
+
// Generate flex properties based on layout
|
| 21 |
+
const getFlexProperties = () => {
|
| 22 |
+
switch (layout) {
|
| 23 |
+
case "2-column":
|
| 24 |
+
return { flexBasis: "50%", maxWidth: "50%" };
|
| 25 |
+
case "3-column":
|
| 26 |
+
return { flexBasis: "33.333%", maxWidth: "33.333%" };
|
| 27 |
+
case "4-column":
|
| 28 |
+
return { flexBasis: "25%", maxWidth: "25%" };
|
| 29 |
+
case "auto":
|
| 30 |
+
return { flexBasis: "auto", maxWidth: "none" };
|
| 31 |
+
default:
|
| 32 |
+
// By default, all children on one line with equal width
|
| 33 |
+
return { flexBasis: "auto", maxWidth: "none" };
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
const getGapSize = () => {
|
| 38 |
+
// If it's a predefined size, return the corresponding value
|
| 39 |
+
switch (gap) {
|
| 40 |
+
case "small":
|
| 41 |
+
return "0.5rem";
|
| 42 |
+
case "medium":
|
| 43 |
+
return "1rem";
|
| 44 |
+
case "large":
|
| 45 |
+
return "1.5rem";
|
| 46 |
+
default:
|
| 47 |
+
// If it's a custom value, return it as-is (e.g., "2rem", "20px", "1.5em")
|
| 48 |
+
return gap;
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
const flexProps = getFlexProperties();
|
| 53 |
+
const gapSize = getGapSize();
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
<div
|
| 57 |
+
class={`stack ${className || ""}`}
|
| 58 |
+
data-layout={layout}
|
| 59 |
+
data-gap={gap}
|
| 60 |
+
{id}
|
| 61 |
+
style={`gap: ${gapSize}`}
|
| 62 |
+
>
|
| 63 |
+
<slot />
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<style>
|
| 67 |
+
.stack {
|
| 68 |
+
display: grid;
|
| 69 |
+
gap: 1rem;
|
| 70 |
+
margin: var(--block-spacing-y) 0;
|
| 71 |
+
width: 100%;
|
| 72 |
+
max-width: 100%;
|
| 73 |
+
box-sizing: border-box;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Layout configurations */
|
| 77 |
+
.stack[data-layout="2-column"] {
|
| 78 |
+
grid-template-columns: repeat(2, 1fr);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.stack[data-layout="3-column"] {
|
| 82 |
+
grid-template-columns: repeat(3, 1fr);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.stack[data-layout="4-column"] {
|
| 86 |
+
grid-template-columns: repeat(4, 1fr);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.stack[data-layout="auto"] {
|
| 90 |
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Default layout (2-column) */
|
| 94 |
+
.stack:not([data-layout]) {
|
| 95 |
+
grid-template-columns: repeat(2, 1fr);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Ensure child elements don't overflow */
|
| 99 |
+
.stack :global(> *) {
|
| 100 |
+
min-width: 0 !important;
|
| 101 |
+
max-width: 100% !important;
|
| 102 |
+
box-sizing: border-box !important;
|
| 103 |
+
word-wrap: break-word !important;
|
| 104 |
+
overflow-wrap: break-word !important;
|
| 105 |
+
overflow: hidden !important;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* Handle code blocks inside stack */
|
| 109 |
+
.stack pre {
|
| 110 |
+
overflow-x: auto;
|
| 111 |
+
max-width: 100%;
|
| 112 |
+
width: 100%;
|
| 113 |
+
word-wrap: break-word;
|
| 114 |
+
white-space: pre-wrap;
|
| 115 |
+
box-sizing: border-box;
|
| 116 |
+
min-width: 0 !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.stack code {
|
| 120 |
+
word-wrap: break-word;
|
| 121 |
+
white-space: pre-wrap;
|
| 122 |
+
max-width: 100%;
|
| 123 |
+
box-sizing: border-box;
|
| 124 |
+
min-width: 0 !important;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* Override the min-width: 100% from _code.css */
|
| 128 |
+
.stack pre code {
|
| 129 |
+
min-width: 0 !important;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* Override section.content-grid pre code min-width rule */
|
| 133 |
+
.stack section.content-grid pre code {
|
| 134 |
+
min-width: 0 !important;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
/* Responsive behavior */
|
| 138 |
+
@media (max-width: 768px) {
|
| 139 |
+
.stack[data-layout="3-column"],
|
| 140 |
+
.stack[data-layout="4-column"],
|
| 141 |
+
.stack[data-layout="2-column"],
|
| 142 |
+
.stack[data-layout="auto"],
|
| 143 |
+
.stack:not([data-layout]) {
|
| 144 |
+
grid-template-columns: 1fr !important;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
@media (min-width: 769px) and (max-width: 1100px) {
|
| 149 |
+
.stack[data-layout="3-column"],
|
| 150 |
+
.stack[data-layout="4-column"],
|
| 151 |
+
.stack[data-layout="auto"] {
|
| 152 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
@media (min-width: 1101px) and (max-width: 1400px) {
|
| 157 |
+
.stack[data-layout="4-column"] {
|
| 158 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
</style>
|
app/src/components/demo/ColorPicker.astro
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
<div class="color-picker" style="width:100%; margin: 10px 0;">
|
| 6 |
+
<style>
|
| 7 |
+
.color-picker .picker__stack {
|
| 8 |
+
display: flex;
|
| 9 |
+
flex-direction: column;
|
| 10 |
+
gap: 12px;
|
| 11 |
+
}
|
| 12 |
+
.color-picker .current-card {
|
| 13 |
+
display: grid;
|
| 14 |
+
grid-template-columns: 40% 60%;
|
| 15 |
+
align-items: center;
|
| 16 |
+
gap: 14px;
|
| 17 |
+
padding: 14px 32px 14px 16px;
|
| 18 |
+
border: 1px solid var(--border-color);
|
| 19 |
+
background: var(--surface-bg);
|
| 20 |
+
border-radius: 12px;
|
| 21 |
+
}
|
| 22 |
+
.color-picker .current-left {
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
gap: 8px;
|
| 26 |
+
min-width: 0;
|
| 27 |
+
}
|
| 28 |
+
.color-picker .current-right {
|
| 29 |
+
display: flex;
|
| 30 |
+
flex-direction: column;
|
| 31 |
+
gap: 8px;
|
| 32 |
+
padding-left: 14px;
|
| 33 |
+
border-left: 1px solid var(--border-color);
|
| 34 |
+
}
|
| 35 |
+
.color-picker .current-main {
|
| 36 |
+
display: flex;
|
| 37 |
+
align-items: center;
|
| 38 |
+
gap: 12px;
|
| 39 |
+
min-width: 0;
|
| 40 |
+
}
|
| 41 |
+
.color-picker .current-swatch {
|
| 42 |
+
width: 64px;
|
| 43 |
+
height: 64px;
|
| 44 |
+
border-radius: 8px;
|
| 45 |
+
border: 1px solid var(--border-color);
|
| 46 |
+
}
|
| 47 |
+
.color-picker .current-text {
|
| 48 |
+
display: flex;
|
| 49 |
+
flex-direction: column;
|
| 50 |
+
line-height: 1.2;
|
| 51 |
+
min-width: 0;
|
| 52 |
+
}
|
| 53 |
+
.color-picker .current-name {
|
| 54 |
+
font-size: 14px;
|
| 55 |
+
font-weight: 800;
|
| 56 |
+
color: var(--text-color);
|
| 57 |
+
white-space: nowrap;
|
| 58 |
+
overflow: hidden;
|
| 59 |
+
text-overflow: ellipsis;
|
| 60 |
+
max-width: clamp(140px, 28vw, 260px);
|
| 61 |
+
}
|
| 62 |
+
.color-picker .current-hex,
|
| 63 |
+
.color-picker .current-extra {
|
| 64 |
+
font-size: 11px;
|
| 65 |
+
color: var(--muted-color);
|
| 66 |
+
letter-spacing: 0.02em;
|
| 67 |
+
white-space: nowrap;
|
| 68 |
+
overflow: hidden;
|
| 69 |
+
text-overflow: ellipsis;
|
| 70 |
+
max-width: clamp(140px, 28vw, 260px);
|
| 71 |
+
}
|
| 72 |
+
:global(.color-label) {
|
| 73 |
+
color: var(--muted-color);
|
| 74 |
+
opacity: 0.6 !important;
|
| 75 |
+
}
|
| 76 |
+
:global(.color-value) {
|
| 77 |
+
color: var(--text-color);
|
| 78 |
+
font-weight: 500;
|
| 79 |
+
}
|
| 80 |
+
.color-picker .picker__label {
|
| 81 |
+
font-weight: 700;
|
| 82 |
+
font-size: 12px;
|
| 83 |
+
color: var(--muted-color);
|
| 84 |
+
text-transform: uppercase;
|
| 85 |
+
letter-spacing: 0.02em;
|
| 86 |
+
}
|
| 87 |
+
.color-picker .hue-slider {
|
| 88 |
+
position: relative;
|
| 89 |
+
height: 16px;
|
| 90 |
+
border-radius: 10px;
|
| 91 |
+
border: 1px solid var(--border-color);
|
| 92 |
+
background: linear-gradient(
|
| 93 |
+
to right,
|
| 94 |
+
#f00 0%,
|
| 95 |
+
#ff0 17%,
|
| 96 |
+
#0f0 33%,
|
| 97 |
+
#0ff 50%,
|
| 98 |
+
#00f 67%,
|
| 99 |
+
#f0f 83%,
|
| 100 |
+
#f00 100%
|
| 101 |
+
);
|
| 102 |
+
cursor: ew-resize;
|
| 103 |
+
touch-action: none;
|
| 104 |
+
flex: 1 1 auto;
|
| 105 |
+
min-width: 200px;
|
| 106 |
+
}
|
| 107 |
+
.color-picker .hue-knob {
|
| 108 |
+
position: absolute;
|
| 109 |
+
top: 50%;
|
| 110 |
+
left: 93.6%;
|
| 111 |
+
width: 14px;
|
| 112 |
+
height: 14px;
|
| 113 |
+
border-radius: 50%;
|
| 114 |
+
border: 2px solid #fff;
|
| 115 |
+
transform: translate(-50%, -50%);
|
| 116 |
+
background: var(--surface-bg);
|
| 117 |
+
z-index: 2;
|
| 118 |
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
| 119 |
+
}
|
| 120 |
+
.color-picker .hue-slider:focus-visible {
|
| 121 |
+
outline: 2px solid var(--primary-color);
|
| 122 |
+
outline-offset: 2px;
|
| 123 |
+
}
|
| 124 |
+
.color-picker .hue-value {
|
| 125 |
+
font-variant-numeric: tabular-nums;
|
| 126 |
+
color: var(--muted-color);
|
| 127 |
+
font-size: 12px;
|
| 128 |
+
}
|
| 129 |
+
@media (max-width: 720px) {
|
| 130 |
+
.color-picker .current-card {
|
| 131 |
+
grid-template-columns: 1fr;
|
| 132 |
+
}
|
| 133 |
+
.color-picker .current-right {
|
| 134 |
+
padding-left: 0;
|
| 135 |
+
border-left: none;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
</style>
|
| 139 |
+
<div class="picker__stack">
|
| 140 |
+
<div class="current-card">
|
| 141 |
+
<div class="current-left">
|
| 142 |
+
<div class="current-main">
|
| 143 |
+
<div
|
| 144 |
+
class="current-swatch"
|
| 145 |
+
aria-label="Current color"
|
| 146 |
+
title="Current color"
|
| 147 |
+
>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="current-text">
|
| 150 |
+
<div class="current-name">—</div>
|
| 151 |
+
<div class="current-extra current-lch">—</div>
|
| 152 |
+
<div class="current-extra current-rgb">—</div>
|
| 153 |
+
<div class="current-hex">—</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="current-right">
|
| 158 |
+
<div class="picker__label">Hue</div>
|
| 159 |
+
<div
|
| 160 |
+
class="hue-slider"
|
| 161 |
+
role="slider"
|
| 162 |
+
aria-label="Hue"
|
| 163 |
+
aria-valuemin="0"
|
| 164 |
+
aria-valuemax="360"
|
| 165 |
+
aria-valuenow="214"
|
| 166 |
+
tabindex="0"
|
| 167 |
+
>
|
| 168 |
+
<div class="hue-knob"></div>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="hue-value">214°</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
<script>
|
| 176 |
+
(() => {
|
| 177 |
+
const COLOR_NAMES = [
|
| 178 |
+
{ name: "Candy Apple Red", hex: "#ff0800" },
|
| 179 |
+
{ name: "Boiling Magma", hex: "#ff3300" },
|
| 180 |
+
{ name: "Aerospace Orange", hex: "#ff4f00" },
|
| 181 |
+
{ name: "Burtuqali Orange", hex: "#ff6700" },
|
| 182 |
+
{ name: "American Orange", hex: "#ff8b00" },
|
| 183 |
+
{ name: "Cheese", hex: "#ffa600" },
|
| 184 |
+
{ name: "Amber", hex: "#ffbf00" },
|
| 185 |
+
{ name: "Demonic Yellow", hex: "#ffe700" },
|
| 186 |
+
{ name: "Bat-Signal", hex: "#feff00" },
|
| 187 |
+
{ name: "Bitter Lime", hex: "#cfff00" },
|
| 188 |
+
{ name: "Electric Lime", hex: "#ccff00" },
|
| 189 |
+
{ name: "Bright Yellow Green", hex: "#9dff00" },
|
| 190 |
+
{ name: "Lasting Lime", hex: "#88ff00" },
|
| 191 |
+
{ name: "Bright Green", hex: "#66ff00" },
|
| 192 |
+
{ name: "Chlorophyll Green", hex: "#4aff00" },
|
| 193 |
+
{ name: "Green Screen", hex: "#22ff00" },
|
| 194 |
+
{ name: "Electric Pickle", hex: "#00ff04" },
|
| 195 |
+
{ name: "Acid", hex: "#00ff22" },
|
| 196 |
+
{ name: "Lucent Lime", hex: "#00ff33" },
|
| 197 |
+
{ name: "Cathode Green", hex: "#00ff55" },
|
| 198 |
+
{ name: "Booger Buster", hex: "#00ff77" },
|
| 199 |
+
{ name: "Green Gas", hex: "#00ff99" },
|
| 200 |
+
{ name: "Enthusiasm", hex: "#00ffaa" },
|
| 201 |
+
{ name: "Ice Ice Baby", hex: "#00ffdd" },
|
| 202 |
+
{ name: "Master Sword Blue", hex: "#00ffee" },
|
| 203 |
+
{ name: "Agressive Aqua", hex: "#00fbff" },
|
| 204 |
+
{ name: "Vivid Sky Blue", hex: "#00ccff" },
|
| 205 |
+
{ name: "Capri", hex: "#00bfff" },
|
| 206 |
+
{ name: "Sky of Magritte", hex: "#0099ff" },
|
| 207 |
+
{ name: "Azure", hex: "#007fff" },
|
| 208 |
+
{ name: "Blue Ribbon", hex: "#0066ff" },
|
| 209 |
+
{ name: "Blinking Blue", hex: "#0033ff" },
|
| 210 |
+
{ name: "Icelandic Water", hex: "#0011ff" },
|
| 211 |
+
{ name: "Blue", hex: "#0000ff" },
|
| 212 |
+
{ name: "Blue Pencil", hex: "#2200ff" },
|
| 213 |
+
{ name: "Electric Ultramarine", hex: "#3f00ff" },
|
| 214 |
+
{ name: "Aladdin's Feather", hex: "#5500ff" },
|
| 215 |
+
{ name: "Purple Climax", hex: "#8800ff" },
|
| 216 |
+
{ name: "Amethyst Ganzstar", hex: "#8f00ff" },
|
| 217 |
+
{ name: "Electric Purple", hex: "#bf00ff" },
|
| 218 |
+
{ name: "Phlox", hex: "#df00ff" },
|
| 219 |
+
{ name: "Brusque Pink", hex: "#ee00ff" },
|
| 220 |
+
{ name: "Bright Magenta", hex: "#ff08e8" },
|
| 221 |
+
{ name: "Big bang Pink", hex: "#ff00bb" },
|
| 222 |
+
{ name: "Mean Girls Lipstick", hex: "#ff00ae" },
|
| 223 |
+
{ name: "Pink", hex: "#ff0099" },
|
| 224 |
+
{ name: "Hot Flamingoes", hex: "#ff005d" },
|
| 225 |
+
{ name: "Blazing Dragonfruit", hex: "#ff0054" },
|
| 226 |
+
{ name: "Carmine Red", hex: "#ff0038" },
|
| 227 |
+
{ name: "Bright Red", hex: "#ff000d" },
|
| 228 |
+
];
|
| 229 |
+
if (!window.__colorNames) window.__colorNames = COLOR_NAMES;
|
| 230 |
+
|
| 231 |
+
if (!window.__colorPickerBus) {
|
| 232 |
+
window.__colorPickerBus = (() => {
|
| 233 |
+
let hue = 214;
|
| 234 |
+
let adjusting = false;
|
| 235 |
+
const listeners = new Set();
|
| 236 |
+
return {
|
| 237 |
+
get: () => ({ hue, adjusting }),
|
| 238 |
+
publish: (sourceId, nextHue, isAdj) => {
|
| 239 |
+
hue = ((nextHue % 360) + 360) % 360;
|
| 240 |
+
adjusting = !!isAdj;
|
| 241 |
+
listeners.forEach((fn) => {
|
| 242 |
+
try {
|
| 243 |
+
fn({ sourceId, hue, adjusting });
|
| 244 |
+
} catch {}
|
| 245 |
+
});
|
| 246 |
+
},
|
| 247 |
+
subscribe: (fn) => {
|
| 248 |
+
listeners.add(fn);
|
| 249 |
+
return () => listeners.delete(fn);
|
| 250 |
+
},
|
| 251 |
+
};
|
| 252 |
+
})();
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
const bootstrap = () => {
|
| 256 |
+
const root = document.querySelector(".color-picker");
|
| 257 |
+
if (!root || root.dataset.mounted) return;
|
| 258 |
+
root.dataset.mounted = "true";
|
| 259 |
+
const slider = root.querySelector(".hue-slider");
|
| 260 |
+
const knob = root.querySelector(".hue-knob");
|
| 261 |
+
const hueValue = root.querySelector(".hue-value");
|
| 262 |
+
const currentSwatch = root.querySelector(".current-swatch");
|
| 263 |
+
const currentName = root.querySelector(".current-name");
|
| 264 |
+
const currentHex = root.querySelector(".current-hex");
|
| 265 |
+
const currentLch = root.querySelector(".current-lch");
|
| 266 |
+
const currentRgb = root.querySelector(".current-rgb");
|
| 267 |
+
const bus = window.__colorPickerBus;
|
| 268 |
+
const instanceId = Math.random().toString(36).slice(2);
|
| 269 |
+
const getKnobRadius = () => {
|
| 270 |
+
try {
|
| 271 |
+
const w = knob ? knob.getBoundingClientRect().width : 0;
|
| 272 |
+
return w ? w / 2 : 8;
|
| 273 |
+
} catch {
|
| 274 |
+
return 8;
|
| 275 |
+
}
|
| 276 |
+
};
|
| 277 |
+
const hexToHsl = (H) => {
|
| 278 |
+
const s = H.replace("#", "");
|
| 279 |
+
const v =
|
| 280 |
+
s.length === 3
|
| 281 |
+
? s
|
| 282 |
+
.split("")
|
| 283 |
+
.map((ch) => ch + ch)
|
| 284 |
+
.join("")
|
| 285 |
+
: s;
|
| 286 |
+
const bigint = parseInt(v, 16);
|
| 287 |
+
let r = (bigint >> 16) & 255,
|
| 288 |
+
g = (bigint >> 8) & 255,
|
| 289 |
+
b = bigint & 255;
|
| 290 |
+
r /= 255;
|
| 291 |
+
g /= 255;
|
| 292 |
+
b /= 255;
|
| 293 |
+
const max = Math.max(r, g, b),
|
| 294 |
+
min = Math.min(r, g, b);
|
| 295 |
+
let h = 0,
|
| 296 |
+
s2 = 0,
|
| 297 |
+
l = (max + min) / 2;
|
| 298 |
+
if (max !== min) {
|
| 299 |
+
const d = max - min;
|
| 300 |
+
s2 = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
| 301 |
+
switch (max) {
|
| 302 |
+
case r:
|
| 303 |
+
h = (g - b) / d + (g < b ? 6 : 0);
|
| 304 |
+
break;
|
| 305 |
+
case g:
|
| 306 |
+
h = (b - r) / d + 2;
|
| 307 |
+
break;
|
| 308 |
+
default:
|
| 309 |
+
h = (r - g) / d + 4;
|
| 310 |
+
}
|
| 311 |
+
h /= 6;
|
| 312 |
+
}
|
| 313 |
+
return {
|
| 314 |
+
h: Math.round(h * 360),
|
| 315 |
+
s: Math.round(s2 * 100),
|
| 316 |
+
l: Math.round(l * 100),
|
| 317 |
+
};
|
| 318 |
+
};
|
| 319 |
+
const hslToHex = (h, s, l) => {
|
| 320 |
+
s /= 100;
|
| 321 |
+
l /= 100;
|
| 322 |
+
const k = (n) => (n + h / 30) % 12;
|
| 323 |
+
const a = s * Math.min(l, 1 - l);
|
| 324 |
+
const f = (n) =>
|
| 325 |
+
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
| 326 |
+
const toHex = (x) =>
|
| 327 |
+
Math.round(255 * x)
|
| 328 |
+
.toString(16)
|
| 329 |
+
.padStart(2, "0");
|
| 330 |
+
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`.toUpperCase();
|
| 331 |
+
};
|
| 332 |
+
|
| 333 |
+
// OKLCH conversion functions
|
| 334 |
+
const srgbToLinear = (u) =>
|
| 335 |
+
u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4);
|
| 336 |
+
const linearToSrgb = (u) =>
|
| 337 |
+
u <= 0.0031308
|
| 338 |
+
? 12.92 * u
|
| 339 |
+
: 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055;
|
| 340 |
+
const rgbToOklab = (r, g, b) => {
|
| 341 |
+
const rl = srgbToLinear(r),
|
| 342 |
+
gl = srgbToLinear(g),
|
| 343 |
+
bl = srgbToLinear(b);
|
| 344 |
+
const l = Math.cbrt(
|
| 345 |
+
0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl,
|
| 346 |
+
);
|
| 347 |
+
const m = Math.cbrt(
|
| 348 |
+
0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl,
|
| 349 |
+
);
|
| 350 |
+
const s = Math.cbrt(
|
| 351 |
+
0.0883024619 * rl + 0.2817188366 * gl + 0.6299787005 * bl,
|
| 352 |
+
);
|
| 353 |
+
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
| 354 |
+
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
| 355 |
+
const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
| 356 |
+
return { L, a, b: b2 };
|
| 357 |
+
};
|
| 358 |
+
const oklabToOklch = (L, a, b) => {
|
| 359 |
+
const C = Math.sqrt(a * a + b * b);
|
| 360 |
+
let h = (Math.atan2(b, a) * 180) / Math.PI;
|
| 361 |
+
if (h < 0) h += 360;
|
| 362 |
+
return { L, C, h };
|
| 363 |
+
};
|
| 364 |
+
const hexToOklch = (hex) => {
|
| 365 |
+
const s = hex.replace("#", "");
|
| 366 |
+
const v =
|
| 367 |
+
s.length === 3
|
| 368 |
+
? s
|
| 369 |
+
.split("")
|
| 370 |
+
.map((ch) => ch + ch)
|
| 371 |
+
.join("")
|
| 372 |
+
: s;
|
| 373 |
+
const r = parseInt(v.slice(0, 2), 16) / 255;
|
| 374 |
+
const g = parseInt(v.slice(2, 4), 16) / 255;
|
| 375 |
+
const b = parseInt(v.slice(4, 6), 16) / 255;
|
| 376 |
+
const { L, a, b: bb } = rgbToOklab(r, g, b);
|
| 377 |
+
return oklabToOklch(L, a, bb);
|
| 378 |
+
};
|
| 379 |
+
// Precompute hues for the provided color-name list
|
| 380 |
+
const NAME_HUES = COLOR_NAMES.map((c) => {
|
| 381 |
+
const hh = hexToHsl(c.hex).h || 0;
|
| 382 |
+
return { name: c.name, hue: hh };
|
| 383 |
+
});
|
| 384 |
+
// Pick closest name by circular hue distance; fallback to coarse labels
|
| 385 |
+
const getName = (hex) => {
|
| 386 |
+
const h = hexToHsl(hex).h || 0;
|
| 387 |
+
let bestName = "—";
|
| 388 |
+
let best = 361;
|
| 389 |
+
for (let i = 0; i < NAME_HUES.length; i++) {
|
| 390 |
+
const hh = NAME_HUES[i].hue;
|
| 391 |
+
const d = Math.abs(hh - h);
|
| 392 |
+
const dist = Math.min(d, 360 - d);
|
| 393 |
+
if (dist < best) {
|
| 394 |
+
best = dist;
|
| 395 |
+
bestName = NAME_HUES[i].name;
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
if (bestName !== "—") return bestName;
|
| 399 |
+
const labels = [
|
| 400 |
+
"Red",
|
| 401 |
+
"Orange",
|
| 402 |
+
"Yellow",
|
| 403 |
+
"Lime",
|
| 404 |
+
"Green",
|
| 405 |
+
"Cyan",
|
| 406 |
+
"Blue",
|
| 407 |
+
"Indigo",
|
| 408 |
+
"Violet",
|
| 409 |
+
"Magenta",
|
| 410 |
+
];
|
| 411 |
+
const idx = Math.round(((h % 360) / 360) * (labels.length - 1));
|
| 412 |
+
return labels[idx];
|
| 413 |
+
};
|
| 414 |
+
// OKLCH to RGB conversion functions
|
| 415 |
+
const oklchToOklab = (L, C, hDeg) => {
|
| 416 |
+
const h = (hDeg * Math.PI) / 180;
|
| 417 |
+
return { L, a: C * Math.cos(h), b: C * Math.sin(h) };
|
| 418 |
+
};
|
| 419 |
+
const oklabToRgb = (L, a, b) => {
|
| 420 |
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
| 421 |
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
| 422 |
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
| 423 |
+
const l = l_ * l_ * l_;
|
| 424 |
+
const m = m_ * m_ * m_;
|
| 425 |
+
const s = s_ * s_ * s_;
|
| 426 |
+
const r = linearToSrgb(
|
| 427 |
+
+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
| 428 |
+
);
|
| 429 |
+
const g = linearToSrgb(
|
| 430 |
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
| 431 |
+
);
|
| 432 |
+
const b3 = linearToSrgb(
|
| 433 |
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
| 434 |
+
);
|
| 435 |
+
return { r, g, b: b3 };
|
| 436 |
+
};
|
| 437 |
+
const oklchToHex = (L, C, h) => {
|
| 438 |
+
const { a, b } = oklchToOklab(L, C, h);
|
| 439 |
+
const rgb = oklabToRgb(L, a, b);
|
| 440 |
+
const toHex = (x) =>
|
| 441 |
+
Math.round(255 * x)
|
| 442 |
+
.toString(16)
|
| 443 |
+
.padStart(2, "0");
|
| 444 |
+
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`.toUpperCase();
|
| 445 |
+
};
|
| 446 |
+
|
| 447 |
+
const updateUI = (h, adjusting) => {
|
| 448 |
+
const rect = slider.getBoundingClientRect();
|
| 449 |
+
const r = Math.min(getKnobRadius(), Math.max(0, rect.width / 2 - 1));
|
| 450 |
+
const t = Math.max(0, Math.min(1, h / 360));
|
| 451 |
+
const leftPx = r + t * Math.max(0, rect.width - 2 * r);
|
| 452 |
+
if (knob) knob.style.left = (leftPx / rect.width) * 100 + "%";
|
| 453 |
+
if (hueValue) hueValue.innerHTML = `<strong>${Math.round(h)}°</strong>`;
|
| 454 |
+
if (slider) slider.setAttribute("aria-valuenow", String(Math.round(h)));
|
| 455 |
+
|
| 456 |
+
// Generate OKLCH color directly (similar to CSS variables)
|
| 457 |
+
const L = 0.75; // 75% lightness
|
| 458 |
+
const C = 0.12; // 12% chroma
|
| 459 |
+
const oklchColor = `oklch(${L} ${C} ${h})`;
|
| 460 |
+
const baseHex = oklchToHex(L, C, h);
|
| 461 |
+
|
| 462 |
+
if (currentSwatch) currentSwatch.style.background = baseHex;
|
| 463 |
+
if (currentName) currentName.textContent = getName(baseHex);
|
| 464 |
+
if (currentHex)
|
| 465 |
+
currentHex.innerHTML = `<span class="color-label">#</span><span class="color-value">${baseHex.replace("#", "")}</span>`;
|
| 466 |
+
|
| 467 |
+
// Display OKLCH values
|
| 468 |
+
if (currentLch)
|
| 469 |
+
currentLch.innerHTML = `<span class="color-label">OKLCH</span> <span class="color-value">${(L * 100).toFixed(1)}</span><span class="color-label">%</span>, <span class="color-value">${(C * 100).toFixed(1)}</span><span class="color-label">%</span>, <span class="color-value"><strong>${Math.round(h)}</strong></span><span class="color-label">°</span>`;
|
| 470 |
+
|
| 471 |
+
if (currentRgb) {
|
| 472 |
+
const hex = baseHex.replace("#", "");
|
| 473 |
+
const R = parseInt(hex.slice(0, 2), 16),
|
| 474 |
+
G = parseInt(hex.slice(2, 4), 16),
|
| 475 |
+
B = parseInt(hex.slice(4, 6), 16);
|
| 476 |
+
currentRgb.innerHTML = `<span class="color-label">RGB</span> <span class="color-value">${R}</span><span class="color-label">,</span> <span class="color-value">${G}</span><span class="color-label">,</span> <span class="color-value">${B}</span>`;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
// Apply OKLCH color to CSS variables
|
| 480 |
+
const hoverOklch = `oklch(${L} ${C} ${h})`;
|
| 481 |
+
const rootEl = document.documentElement;
|
| 482 |
+
rootEl.style.setProperty("--primary-color", oklchColor);
|
| 483 |
+
rootEl.style.setProperty("--primary-color-hover", hoverOklch);
|
| 484 |
+
};
|
| 485 |
+
|
| 486 |
+
// Update UI position only, without modifying CSS colors
|
| 487 |
+
const updateUIPositionOnly = (h) => {
|
| 488 |
+
const rect = slider.getBoundingClientRect();
|
| 489 |
+
const r = Math.min(getKnobRadius(), Math.max(0, rect.width / 2 - 1));
|
| 490 |
+
const t = Math.max(0, Math.min(1, h / 360));
|
| 491 |
+
const leftPx = r + t * Math.max(0, rect.width - 2 * r);
|
| 492 |
+
if (knob) knob.style.left = (leftPx / rect.width) * 100 + "%";
|
| 493 |
+
if (hueValue) hueValue.innerHTML = `<strong>${Math.round(h)}°</strong>`;
|
| 494 |
+
if (slider) slider.setAttribute("aria-valuenow", String(Math.round(h)));
|
| 495 |
+
|
| 496 |
+
// Generate OKLCH color for display only
|
| 497 |
+
const L = 0.75; // 75% lightness
|
| 498 |
+
const C = 0.12; // 12% chroma
|
| 499 |
+
const baseHex = oklchToHex(L, C, h);
|
| 500 |
+
|
| 501 |
+
if (currentSwatch) currentSwatch.style.background = baseHex;
|
| 502 |
+
if (currentName) currentName.textContent = getName(baseHex);
|
| 503 |
+
if (currentHex)
|
| 504 |
+
currentHex.innerHTML = `<span class="color-label">#</span><span class="color-value">${baseHex.replace("#", "")}</span>`;
|
| 505 |
+
|
| 506 |
+
// Display OKLCH values
|
| 507 |
+
if (currentLch)
|
| 508 |
+
currentLch.innerHTML = `<span class="color-label">OKLCH</span> <span class="color-value">${(L * 100).toFixed(1)}</span><span class="color-label">%</span>, <span class="color-value">${(C * 100).toFixed(1)}</span><span class="color-label">%</span>, <span class="color-value"><strong>${Math.round(h)}</strong></span><span class="color-label">°</span>`;
|
| 509 |
+
|
| 510 |
+
if (currentRgb) {
|
| 511 |
+
const hex = baseHex.replace("#", "");
|
| 512 |
+
const R = parseInt(hex.slice(0, 2), 16),
|
| 513 |
+
G = parseInt(hex.slice(2, 4), 16),
|
| 514 |
+
B = parseInt(hex.slice(4, 6), 16);
|
| 515 |
+
currentRgb.innerHTML = `<span class="color-label">RGB</span> <span class="color-value">${R}</span><span class="color-label">,</span> <span class="color-value">${G}</span><span class="color-label">,</span> <span class="color-value">${B}</span>`;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
// DO NOT modify CSS variables - just show the position
|
| 519 |
+
};
|
| 520 |
+
const getHueFromEvent = (ev) => {
|
| 521 |
+
const rect = slider.getBoundingClientRect();
|
| 522 |
+
const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
|
| 523 |
+
const x = clientX - rect.left;
|
| 524 |
+
const r = Math.min(getKnobRadius(), Math.max(0, rect.width / 2 - 1));
|
| 525 |
+
const effX = Math.max(r, Math.min(rect.width - r, x));
|
| 526 |
+
const denom = Math.max(1, rect.width - 2 * r);
|
| 527 |
+
const t = (effX - r) / denom;
|
| 528 |
+
return t * 360;
|
| 529 |
+
};
|
| 530 |
+
const unsubscribe = bus.subscribe(({ sourceId, hue, adjusting }) => {
|
| 531 |
+
if (sourceId === instanceId) return;
|
| 532 |
+
updateUI(hue, adjusting);
|
| 533 |
+
});
|
| 534 |
+
try {
|
| 535 |
+
let initH = 214;
|
| 536 |
+
|
| 537 |
+
// Try to get OKLCH directly from ColorPalettes first
|
| 538 |
+
if (
|
| 539 |
+
window.ColorPalettes &&
|
| 540 |
+
typeof window.ColorPalettes.getPrimaryOKLCH === "function"
|
| 541 |
+
) {
|
| 542 |
+
const oklch = window.ColorPalettes.getPrimaryOKLCH();
|
| 543 |
+
if (oklch && oklch.h !== undefined) {
|
| 544 |
+
initH = oklch.h; // Use exact OKLCH hue, no conversion!
|
| 545 |
+
}
|
| 546 |
+
} else {
|
| 547 |
+
// Fallback: try to parse OKLCH directly from CSS
|
| 548 |
+
const cssPrimary = getComputedStyle(document.documentElement)
|
| 549 |
+
.getPropertyValue("--primary-color")
|
| 550 |
+
.trim();
|
| 551 |
+
if (cssPrimary && cssPrimary.includes("oklch")) {
|
| 552 |
+
const oklchMatch = cssPrimary.match(/oklch\(([^)]+)\)/);
|
| 553 |
+
if (oklchMatch) {
|
| 554 |
+
const values = oklchMatch[1]
|
| 555 |
+
.split(/\s+/)
|
| 556 |
+
.map((v) => parseFloat(v.trim()));
|
| 557 |
+
if (values.length >= 3) {
|
| 558 |
+
initH = values[2]; // Direct OKLCH hue, no conversion!
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
} else if (cssPrimary) {
|
| 562 |
+
// Only convert if it's not OKLCH
|
| 563 |
+
initH = hexToHsl(cssPrimary).h || initH;
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
const { hue: sharedHue } = bus.get();
|
| 568 |
+
// Only update UI position, don't modify CSS colors on initialization
|
| 569 |
+
updateUIPositionOnly(initH ?? sharedHue);
|
| 570 |
+
bus.publish(instanceId, initH ?? sharedHue, false);
|
| 571 |
+
} catch {
|
| 572 |
+
const { hue: sharedHue } = bus.get();
|
| 573 |
+
updateUIPositionOnly(sharedHue);
|
| 574 |
+
}
|
| 575 |
+
const onDown = (ev) => {
|
| 576 |
+
ev.preventDefault();
|
| 577 |
+
const h = getHueFromEvent(ev);
|
| 578 |
+
updateUI(h, true);
|
| 579 |
+
bus.publish(instanceId, h, true);
|
| 580 |
+
const move = (e) => {
|
| 581 |
+
e.preventDefault && e.preventDefault();
|
| 582 |
+
const hh = getHueFromEvent(e);
|
| 583 |
+
updateUI(hh, true);
|
| 584 |
+
bus.publish(instanceId, hh, true);
|
| 585 |
+
};
|
| 586 |
+
const up = () => {
|
| 587 |
+
bus.publish(instanceId, getHueFromEvent(ev), false);
|
| 588 |
+
window.removeEventListener("mousemove", move);
|
| 589 |
+
window.removeEventListener("touchmove", move);
|
| 590 |
+
window.removeEventListener("mouseup", up);
|
| 591 |
+
window.removeEventListener("touchend", up);
|
| 592 |
+
};
|
| 593 |
+
window.addEventListener("mousemove", move, { passive: false });
|
| 594 |
+
window.addEventListener("touchmove", move, { passive: false });
|
| 595 |
+
window.addEventListener("mouseup", up, { once: true });
|
| 596 |
+
window.addEventListener("touchend", up, { once: true });
|
| 597 |
+
};
|
| 598 |
+
if (slider) {
|
| 599 |
+
slider.addEventListener("mousedown", onDown);
|
| 600 |
+
slider.addEventListener("touchstart", onDown, { passive: false });
|
| 601 |
+
slider.addEventListener("keydown", (e) => {
|
| 602 |
+
const step = e.shiftKey ? 10 : 2;
|
| 603 |
+
if (e.key === "ArrowLeft") {
|
| 604 |
+
e.preventDefault();
|
| 605 |
+
const { hue } = bus.get();
|
| 606 |
+
const h = hue - step;
|
| 607 |
+
updateUI(h, true);
|
| 608 |
+
bus.publish(instanceId, h, true);
|
| 609 |
+
bus.publish(instanceId, h, false);
|
| 610 |
+
}
|
| 611 |
+
if (e.key === "ArrowRight") {
|
| 612 |
+
e.preventDefault();
|
| 613 |
+
const { hue } = bus.get();
|
| 614 |
+
const h = hue + step;
|
| 615 |
+
updateUI(h, true);
|
| 616 |
+
bus.publish(instanceId, h, true);
|
| 617 |
+
bus.publish(instanceId, h, false);
|
| 618 |
+
}
|
| 619 |
+
});
|
| 620 |
+
}
|
| 621 |
+
const ro = new MutationObserver(() => {
|
| 622 |
+
if (!document.body.contains(root)) {
|
| 623 |
+
unsubscribe && unsubscribe();
|
| 624 |
+
ro.disconnect();
|
| 625 |
+
}
|
| 626 |
+
});
|
| 627 |
+
ro.observe(document.body, { childList: true, subtree: true });
|
| 628 |
+
};
|
| 629 |
+
if (document.readyState === "loading")
|
| 630 |
+
document.addEventListener("DOMContentLoaded", bootstrap, { once: true });
|
| 631 |
+
else bootstrap();
|
| 632 |
+
})();
|
| 633 |
+
</script>
|
app/src/components/demo/Palettes.astro
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
const rootId = `palettes-${Math.random().toString(36).slice(2)}`;
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
<div class="palettes" id={rootId} style="width:100%; margin: 10px 0;">
|
| 6 |
+
<style is:global>
|
| 7 |
+
.palettes {
|
| 8 |
+
box-sizing: border-box;
|
| 9 |
+
}
|
| 10 |
+
.palettes .palettes__grid {
|
| 11 |
+
display: grid;
|
| 12 |
+
grid-template-columns: 1fr;
|
| 13 |
+
gap: 12px;
|
| 14 |
+
max-width: 100%;
|
| 15 |
+
}
|
| 16 |
+
.palettes .palette-card {
|
| 17 |
+
position: relative;
|
| 18 |
+
display: grid;
|
| 19 |
+
grid-template-columns: 1fr minmax(0, 220px);
|
| 20 |
+
align-items: stretch;
|
| 21 |
+
gap: 12px;
|
| 22 |
+
border: 1px solid var(--border-color);
|
| 23 |
+
border-radius: 10px;
|
| 24 |
+
background: var(--surface-bg);
|
| 25 |
+
padding: 12px;
|
| 26 |
+
transition:
|
| 27 |
+
box-shadow 0.18s ease,
|
| 28 |
+
transform 0.18s ease,
|
| 29 |
+
border-color 0.18s ease;
|
| 30 |
+
min-height: 60px;
|
| 31 |
+
}
|
| 32 |
+
.palettes .palette-card__preview {
|
| 33 |
+
width: 48px;
|
| 34 |
+
height: 48px;
|
| 35 |
+
border-radius: 999px;
|
| 36 |
+
flex: 0 0 auto;
|
| 37 |
+
background-size: cover;
|
| 38 |
+
background-position: center;
|
| 39 |
+
}
|
| 40 |
+
.palettes .palette-card__copy {
|
| 41 |
+
position: absolute;
|
| 42 |
+
top: 50%;
|
| 43 |
+
left: 100%;
|
| 44 |
+
transform: translateY(-50%);
|
| 45 |
+
z-index: 3;
|
| 46 |
+
border-left: none;
|
| 47 |
+
border-top-left-radius: 0;
|
| 48 |
+
border-bottom-left-radius: 0;
|
| 49 |
+
}
|
| 50 |
+
.palettes .palette-card__copy svg {
|
| 51 |
+
width: 18px;
|
| 52 |
+
height: 18px;
|
| 53 |
+
fill: currentColor;
|
| 54 |
+
display: block;
|
| 55 |
+
color: inherit;
|
| 56 |
+
}
|
| 57 |
+
.palettes .palette-card__swatches {
|
| 58 |
+
display: grid;
|
| 59 |
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
| 60 |
+
grid-auto-rows: 1fr;
|
| 61 |
+
gap: 2px;
|
| 62 |
+
margin: 0;
|
| 63 |
+
min-height: 60px;
|
| 64 |
+
}
|
| 65 |
+
.palettes .palette-card__swatches .sw {
|
| 66 |
+
width: 100%;
|
| 67 |
+
min-width: 0;
|
| 68 |
+
height: auto;
|
| 69 |
+
border-radius: 0;
|
| 70 |
+
border: 1px solid var(--border-color);
|
| 71 |
+
}
|
| 72 |
+
.palettes .palette-card__swatches .sw:first-child {
|
| 73 |
+
border-top-left-radius: 8px;
|
| 74 |
+
border-bottom-left-radius: 8px;
|
| 75 |
+
}
|
| 76 |
+
.palettes .palette-card__swatches .sw:last-child {
|
| 77 |
+
border-top-right-radius: 8px;
|
| 78 |
+
border-bottom-right-radius: 8px;
|
| 79 |
+
}
|
| 80 |
+
.palettes .palette-card__content {
|
| 81 |
+
display: flex;
|
| 82 |
+
flex-direction: row;
|
| 83 |
+
align-items: center;
|
| 84 |
+
justify-content: flex-start;
|
| 85 |
+
gap: 12px;
|
| 86 |
+
min-width: 0;
|
| 87 |
+
padding-right: 12px;
|
| 88 |
+
}
|
| 89 |
+
.palettes .palette-card__preview {
|
| 90 |
+
width: 48px;
|
| 91 |
+
height: 48px;
|
| 92 |
+
border-radius: 999px;
|
| 93 |
+
position: relative;
|
| 94 |
+
flex: 0 0 auto;
|
| 95 |
+
overflow: hidden;
|
| 96 |
+
}
|
| 97 |
+
.palettes .palette-card__preview .dot {
|
| 98 |
+
position: absolute;
|
| 99 |
+
width: 4px;
|
| 100 |
+
height: 4px;
|
| 101 |
+
background: #fff;
|
| 102 |
+
border-radius: 999px;
|
| 103 |
+
box-shadow: 0 0 6px rgba(0, 0, 0, 1);
|
| 104 |
+
}
|
| 105 |
+
.palettes .palette-card__preview .donut-hole {
|
| 106 |
+
position: absolute;
|
| 107 |
+
left: 50%;
|
| 108 |
+
top: 50%;
|
| 109 |
+
transform: translate(-50%, -50%);
|
| 110 |
+
width: 24px;
|
| 111 |
+
height: 24px;
|
| 112 |
+
border-radius: 999px;
|
| 113 |
+
background: var(--surface-bg);
|
| 114 |
+
box-shadow: 0 0 0 1px var(--border-color) inset;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.palettes .palette-card__content__info {
|
| 118 |
+
display: flex;
|
| 119 |
+
flex-direction: column;
|
| 120 |
+
}
|
| 121 |
+
.palettes .palette-card__title {
|
| 122 |
+
text-align: left;
|
| 123 |
+
font-weight: 800;
|
| 124 |
+
font-size: 15px;
|
| 125 |
+
}
|
| 126 |
+
.palettes .palette-card__desc {
|
| 127 |
+
text-align: left;
|
| 128 |
+
color: var(--muted-color);
|
| 129 |
+
line-height: 1.5;
|
| 130 |
+
font-size: 12px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.palettes .palettes__select {
|
| 134 |
+
width: 100%;
|
| 135 |
+
max-width: 100%;
|
| 136 |
+
border: 1px solid var(--border-color);
|
| 137 |
+
background: var(--surface-bg);
|
| 138 |
+
color: var(--text-color);
|
| 139 |
+
padding: 8px 10px;
|
| 140 |
+
border-radius: 8px;
|
| 141 |
+
}
|
| 142 |
+
.palettes .sr-only {
|
| 143 |
+
position: absolute;
|
| 144 |
+
width: 1px;
|
| 145 |
+
height: 1px;
|
| 146 |
+
padding: 0;
|
| 147 |
+
margin: -1px;
|
| 148 |
+
overflow: hidden;
|
| 149 |
+
clip: rect(0, 0, 1px, 1px);
|
| 150 |
+
white-space: nowrap;
|
| 151 |
+
border: 0;
|
| 152 |
+
}
|
| 153 |
+
.palettes .palettes__controls {
|
| 154 |
+
display: flex;
|
| 155 |
+
flex-wrap: wrap;
|
| 156 |
+
gap: 16px;
|
| 157 |
+
align-items: center;
|
| 158 |
+
margin: 8px 0 14px;
|
| 159 |
+
}
|
| 160 |
+
.palettes .palettes__field {
|
| 161 |
+
display: flex;
|
| 162 |
+
flex-direction: column;
|
| 163 |
+
gap: 6px;
|
| 164 |
+
min-width: 0;
|
| 165 |
+
flex: 1 1 280px;
|
| 166 |
+
max-width: 100%;
|
| 167 |
+
}
|
| 168 |
+
.palettes .palettes__label {
|
| 169 |
+
font-size: 12px;
|
| 170 |
+
color: var(--muted-color);
|
| 171 |
+
font-weight: 800;
|
| 172 |
+
}
|
| 173 |
+
.palettes .palettes__label-row {
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
justify-content: space-between;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
}
|
| 179 |
+
.palettes .ghost-badge {
|
| 180 |
+
font-size: 11px;
|
| 181 |
+
padding: 1px 6px;
|
| 182 |
+
border-radius: 999px;
|
| 183 |
+
border: 1px solid var(--border-color);
|
| 184 |
+
color: var(--muted-color);
|
| 185 |
+
background: transparent;
|
| 186 |
+
font-variant-numeric: tabular-nums;
|
| 187 |
+
}
|
| 188 |
+
.palettes .palettes__count {
|
| 189 |
+
display: flex;
|
| 190 |
+
align-items: center;
|
| 191 |
+
gap: 8px;
|
| 192 |
+
max-width: 100%;
|
| 193 |
+
}
|
| 194 |
+
.palettes .palettes__count input[type="range"] {
|
| 195 |
+
width: 100%;
|
| 196 |
+
}
|
| 197 |
+
.palettes .palettes__count output {
|
| 198 |
+
min-width: 28px;
|
| 199 |
+
text-align: center;
|
| 200 |
+
font-variant-numeric: tabular-nums;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
color: var(--muted-color);
|
| 203 |
+
}
|
| 204 |
+
.palettes input[type="range"] {
|
| 205 |
+
-webkit-appearance: none;
|
| 206 |
+
appearance: none;
|
| 207 |
+
height: 24px;
|
| 208 |
+
background: transparent;
|
| 209 |
+
cursor: pointer;
|
| 210 |
+
accent-color: var(--primary-color);
|
| 211 |
+
}
|
| 212 |
+
.palettes input[type="range"]:focus {
|
| 213 |
+
outline: none;
|
| 214 |
+
}
|
| 215 |
+
.palettes input[type="range"]::-webkit-slider-runnable-track {
|
| 216 |
+
height: 6px;
|
| 217 |
+
background: var(--border-color);
|
| 218 |
+
border-radius: 999px;
|
| 219 |
+
}
|
| 220 |
+
.palettes input[type="range"]::-webkit-slider-thumb {
|
| 221 |
+
-webkit-appearance: none;
|
| 222 |
+
appearance: none;
|
| 223 |
+
margin-top: -6px;
|
| 224 |
+
width: 18px;
|
| 225 |
+
height: 18px;
|
| 226 |
+
background: var(--primary-color);
|
| 227 |
+
border: 2px solid var(--surface-bg);
|
| 228 |
+
border-radius: 50%;
|
| 229 |
+
}
|
| 230 |
+
.palettes input[type="range"]::-moz-range-track {
|
| 231 |
+
height: 6px;
|
| 232 |
+
background: var(--border-color);
|
| 233 |
+
border: none;
|
| 234 |
+
border-radius: 999px;
|
| 235 |
+
}
|
| 236 |
+
.palettes input[type="range"]::-moz-range-progress {
|
| 237 |
+
height: 6px;
|
| 238 |
+
background: var(--primary-color);
|
| 239 |
+
border-radius: 999px;
|
| 240 |
+
}
|
| 241 |
+
.palettes input[type="range"]::-moz-range-thumb {
|
| 242 |
+
width: 18px;
|
| 243 |
+
height: 18px;
|
| 244 |
+
background: var(--primary-color);
|
| 245 |
+
border: 2px solid var(--surface-bg);
|
| 246 |
+
border-radius: 50%;
|
| 247 |
+
}
|
| 248 |
+
html.cb-grayscale,
|
| 249 |
+
body.cb-grayscale {
|
| 250 |
+
filter: grayscale(1) !important;
|
| 251 |
+
}
|
| 252 |
+
html.cb-protanopia,
|
| 253 |
+
body.cb-protanopia {
|
| 254 |
+
filter: url(#cb-protanopia) !important;
|
| 255 |
+
}
|
| 256 |
+
html.cb-deuteranopia,
|
| 257 |
+
body.cb-deuteranopia {
|
| 258 |
+
filter: url(#cb-deuteranopia) !important;
|
| 259 |
+
}
|
| 260 |
+
html.cb-tritanopia,
|
| 261 |
+
body.cb-tritanopia {
|
| 262 |
+
filter: url(#cb-tritanopia) !important;
|
| 263 |
+
}
|
| 264 |
+
html.cb-achromatopsia,
|
| 265 |
+
body.cb-achromatopsia {
|
| 266 |
+
filter: url(#cb-achromatopsia) !important;
|
| 267 |
+
}
|
| 268 |
+
@media (max-width: 1100px) {
|
| 269 |
+
.palettes .palette-card {
|
| 270 |
+
grid-template-columns: 1fr;
|
| 271 |
+
align-items: stretch;
|
| 272 |
+
gap: 10px;
|
| 273 |
+
}
|
| 274 |
+
.palettes .palette-card__swatches {
|
| 275 |
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
| 276 |
+
}
|
| 277 |
+
.palettes .palette-card__content {
|
| 278 |
+
border-right: none;
|
| 279 |
+
padding-right: 0;
|
| 280 |
+
}
|
| 281 |
+
.palettes .palette-card__copy {
|
| 282 |
+
display: none;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
</style>
|
| 286 |
+
<div class="palettes__controls">
|
| 287 |
+
<div class="palettes__field">
|
| 288 |
+
<label class="palettes__label" for="cb-select"
|
| 289 |
+
>Color vision simulation</label
|
| 290 |
+
>
|
| 291 |
+
<select id="cb-select" class="palettes__select">
|
| 292 |
+
<option value="none"
|
| 293 |
+
>Normal color vision — typical for most people</option
|
| 294 |
+
>
|
| 295 |
+
<option value="achromatopsia">Achromatopsia — no color at all</option>
|
| 296 |
+
<option value="protanopia">Protanopia — reduced/absent reds</option>
|
| 297 |
+
<option value="deuteranopia"
|
| 298 |
+
>Deuteranopia — reduced/absent greens</option
|
| 299 |
+
>
|
| 300 |
+
<option value="tritanopia">Tritanopia — reduced/absent blues</option>
|
| 301 |
+
</select>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="palettes__field">
|
| 304 |
+
<div class="palettes__label-row">
|
| 305 |
+
<label class="palettes__label" for="color-count">Number of colors</label
|
| 306 |
+
>
|
| 307 |
+
<output id="color-count-out" for="color-count" class="ghost-badge"
|
| 308 |
+
>8</output
|
| 309 |
+
>
|
| 310 |
+
</div>
|
| 311 |
+
<div class="palettes__count">
|
| 312 |
+
<input
|
| 313 |
+
id="color-count"
|
| 314 |
+
type="range"
|
| 315 |
+
min="6"
|
| 316 |
+
max="10"
|
| 317 |
+
step="1"
|
| 318 |
+
value="8"
|
| 319 |
+
aria-label="Number of colors"
|
| 320 |
+
/>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
<div class="palettes__grid"></div>
|
| 325 |
+
<div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
|
| 326 |
+
<svg
|
| 327 |
+
aria-hidden="true"
|
| 328 |
+
focusable="false"
|
| 329 |
+
width="0"
|
| 330 |
+
height="0"
|
| 331 |
+
style="position:absolute; left:-9999px; overflow:hidden;"
|
| 332 |
+
>
|
| 333 |
+
<defs>
|
| 334 |
+
<filter id="cb-protanopia"
|
| 335 |
+
><feColorMatrix
|
| 336 |
+
type="matrix"
|
| 337 |
+
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"
|
| 338 |
+
></feColorMatrix></filter
|
| 339 |
+
>
|
| 340 |
+
<filter id="cb-deuteranopia"
|
| 341 |
+
><feColorMatrix
|
| 342 |
+
type="matrix"
|
| 343 |
+
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"
|
| 344 |
+
></feColorMatrix></filter
|
| 345 |
+
>
|
| 346 |
+
<filter id="cb-tritanopia"
|
| 347 |
+
><feColorMatrix
|
| 348 |
+
type="matrix"
|
| 349 |
+
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"
|
| 350 |
+
></feColorMatrix></filter
|
| 351 |
+
>
|
| 352 |
+
<filter id="cb-achromatopsia"
|
| 353 |
+
><feColorMatrix
|
| 354 |
+
type="matrix"
|
| 355 |
+
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"
|
| 356 |
+
></feColorMatrix></filter
|
| 357 |
+
>
|
| 358 |
+
</defs>
|
| 359 |
+
</svg>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
<script type="module" is:inline>
|
| 363 |
+
import "/scripts/color-palettes.js";
|
| 364 |
+
const ROOT_ID = "{rootId}";
|
| 365 |
+
(() => {
|
| 366 |
+
const cards = [
|
| 367 |
+
{
|
| 368 |
+
key: "categorical",
|
| 369 |
+
title: "Categorical",
|
| 370 |
+
desc: "For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors.",
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
key: "sequential",
|
| 374 |
+
title: "Sequential",
|
| 375 |
+
desc: "For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.",
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
key: "diverging",
|
| 379 |
+
title: "Diverging",
|
| 380 |
+
desc: "For numeric scales with negative and positive; Opposing extremes with smooth contrast around a neutral midpoint.",
|
| 381 |
+
},
|
| 382 |
+
];
|
| 383 |
+
const getPaletteColors = (key, count) => {
|
| 384 |
+
const total = Number(count) || 6;
|
| 385 |
+
if (
|
| 386 |
+
window.ColorPalettes &&
|
| 387 |
+
typeof window.ColorPalettes.getColors === "function"
|
| 388 |
+
) {
|
| 389 |
+
return window.ColorPalettes.getColors(key, total) || [];
|
| 390 |
+
}
|
| 391 |
+
return [];
|
| 392 |
+
};
|
| 393 |
+
const render = () => {
|
| 394 |
+
const root =
|
| 395 |
+
document.getElementById(ROOT_ID) || document.querySelector(".palettes");
|
| 396 |
+
if (!root) return;
|
| 397 |
+
const grid = root.querySelector(".palettes__grid");
|
| 398 |
+
if (!grid) return;
|
| 399 |
+
const input = document.getElementById("color-count");
|
| 400 |
+
const total = input ? Number(input.value) || 6 : 6;
|
| 401 |
+
const html = cards
|
| 402 |
+
.map((c) => {
|
| 403 |
+
const colors = getPaletteColors(c.key, total);
|
| 404 |
+
const swatches = colors
|
| 405 |
+
.map(
|
| 406 |
+
(col) => `<div class=\"sw\" style=\"background:${col}\"></div>`,
|
| 407 |
+
)
|
| 408 |
+
.join("");
|
| 409 |
+
const baseHex =
|
| 410 |
+
window.ColorPalettes &&
|
| 411 |
+
typeof window.ColorPalettes.getPrimary === "function"
|
| 412 |
+
? window.ColorPalettes.getPrimary()
|
| 413 |
+
: colors[0] || "#FF0000";
|
| 414 |
+
const hueDeg = (() => {
|
| 415 |
+
try {
|
| 416 |
+
const s = baseHex.replace("#", "");
|
| 417 |
+
const v =
|
| 418 |
+
s.length === 3
|
| 419 |
+
? s
|
| 420 |
+
.split("")
|
| 421 |
+
.map((ch) => ch + ch)
|
| 422 |
+
.join("")
|
| 423 |
+
: s;
|
| 424 |
+
const r = parseInt(v.slice(0, 2), 16) / 255,
|
| 425 |
+
g = parseInt(v.slice(2, 4), 16) / 255,
|
| 426 |
+
b = parseInt(v.slice(4, 6), 16) / 255;
|
| 427 |
+
const M = Math.max(r, g, b),
|
| 428 |
+
m = Math.min(r, g, b),
|
| 429 |
+
d = M - m;
|
| 430 |
+
if (d === 0) return 0;
|
| 431 |
+
let h = 0;
|
| 432 |
+
if (M === r) h = ((g - b) / d) % 6;
|
| 433 |
+
else if (M === g) h = (b - r) / d + 2;
|
| 434 |
+
else h = (r - g) / d + 4;
|
| 435 |
+
h *= 60;
|
| 436 |
+
if (h < 0) h += 360;
|
| 437 |
+
return h;
|
| 438 |
+
} catch {
|
| 439 |
+
return 0;
|
| 440 |
+
}
|
| 441 |
+
})();
|
| 442 |
+
const gradient =
|
| 443 |
+
c.key === "categorical"
|
| 444 |
+
? (() => {
|
| 445 |
+
const steps = 60; // smooth hue wheel (fixed orientation)
|
| 446 |
+
const wheel = Array.from(
|
| 447 |
+
{ length: steps },
|
| 448 |
+
(_, i) =>
|
| 449 |
+
`hsl(${Math.round((i / steps) * 360)}, 100%, 50%)`,
|
| 450 |
+
).join(", ");
|
| 451 |
+
return `conic-gradient(${wheel})`;
|
| 452 |
+
})()
|
| 453 |
+
: colors.length
|
| 454 |
+
? `linear-gradient(90deg, ${colors.join(", ")})`
|
| 455 |
+
: `linear-gradient(90deg, var(--border-color), var(--border-color))`;
|
| 456 |
+
const previewInner = (() => {
|
| 457 |
+
if (c.key !== "categorical" || !colors.length) return "";
|
| 458 |
+
const ring = 18;
|
| 459 |
+
const cx = 24;
|
| 460 |
+
const cy = 24;
|
| 461 |
+
const offset = (hueDeg / 360) * 2 * Math.PI;
|
| 462 |
+
return colors
|
| 463 |
+
.map((col, i) => {
|
| 464 |
+
const angle = offset + (i / colors.length) * 2 * Math.PI;
|
| 465 |
+
const x = cx + ring * Math.cos(angle);
|
| 466 |
+
const y = cy + ring * Math.sin(angle);
|
| 467 |
+
return `<span class=\"dot\" style=\"left:${x - 2}px; top:${y - 2}px\"></span>`;
|
| 468 |
+
})
|
| 469 |
+
.join("");
|
| 470 |
+
})();
|
| 471 |
+
const donutHole =
|
| 472 |
+
c.key === "categorical" ? '<span class="donut-hole"></span>' : "";
|
| 473 |
+
return `
|
| 474 |
+
<div class="palette-card" data-colors="${colors.join(",")}">
|
| 475 |
+
<div class="palette-card__content">
|
| 476 |
+
<div class=\"palette-card__preview\" aria-hidden=\"true\" style=\"background:${gradient}\">${previewInner}${donutHole}</div>
|
| 477 |
+
<div class="palette-card__content__info">
|
| 478 |
+
<div class="palette-card__title">${c.title}</div>
|
| 479 |
+
<div class="palette-card__desc">${c.desc}</div>
|
| 480 |
+
</div>
|
| 481 |
+
</div>
|
| 482 |
+
<div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div>
|
| 483 |
+
<button class="palette-card__copy button--ghost" type="button" aria-label="Copy palette">
|
| 484 |
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
| 485 |
+
</button>
|
| 486 |
+
</div>`;
|
| 487 |
+
})
|
| 488 |
+
.join("");
|
| 489 |
+
grid.innerHTML = html;
|
| 490 |
+
};
|
| 491 |
+
const MODE_TO_CLASS = {
|
| 492 |
+
protanopia: "cb-protanopia",
|
| 493 |
+
deuteranopia: "cb-deuteranopia",
|
| 494 |
+
tritanopia: "cb-tritanopia",
|
| 495 |
+
achromatopsia: "cb-achromatopsia",
|
| 496 |
+
};
|
| 497 |
+
const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
|
| 498 |
+
const clearCbClasses = () => {
|
| 499 |
+
const rootEl = document.documentElement;
|
| 500 |
+
CLEAR_CLASSES.forEach((cls) => rootEl.classList.remove(cls));
|
| 501 |
+
};
|
| 502 |
+
const applyCbClass = (mode) => {
|
| 503 |
+
clearCbClasses();
|
| 504 |
+
const cls = MODE_TO_CLASS[mode];
|
| 505 |
+
if (cls) document.documentElement.classList.add(cls);
|
| 506 |
+
};
|
| 507 |
+
const currentCbMode = () => {
|
| 508 |
+
const rootEl = document.documentElement;
|
| 509 |
+
for (const [mode, cls] of Object.entries(MODE_TO_CLASS)) {
|
| 510 |
+
if (rootEl.classList.contains(cls)) return mode;
|
| 511 |
+
}
|
| 512 |
+
return "none";
|
| 513 |
+
};
|
| 514 |
+
const setupCbSim = () => {
|
| 515 |
+
const select = document.getElementById("cb-select");
|
| 516 |
+
if (!select) return;
|
| 517 |
+
try {
|
| 518 |
+
select.value = currentCbMode();
|
| 519 |
+
} catch {}
|
| 520 |
+
select.addEventListener("change", () => applyCbClass(select.value));
|
| 521 |
+
};
|
| 522 |
+
const setupCountControl = () => {
|
| 523 |
+
const input = document.getElementById("color-count");
|
| 524 |
+
const out = document.getElementById("color-count-out");
|
| 525 |
+
if (!input) return;
|
| 526 |
+
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
| 527 |
+
const read = () => clamp(Number(input.value) || 6, 6, 10);
|
| 528 |
+
const syncOut = () => {
|
| 529 |
+
if (out) out.textContent = String(read());
|
| 530 |
+
};
|
| 531 |
+
const onChange = () => {
|
| 532 |
+
syncOut();
|
| 533 |
+
render();
|
| 534 |
+
};
|
| 535 |
+
syncOut();
|
| 536 |
+
input.addEventListener("input", onChange);
|
| 537 |
+
document.addEventListener("palettes:updated", () => {
|
| 538 |
+
syncOut();
|
| 539 |
+
render();
|
| 540 |
+
});
|
| 541 |
+
};
|
| 542 |
+
let copyDelegationSetup = false;
|
| 543 |
+
const setupCopyDelegation = () => {
|
| 544 |
+
if (copyDelegationSetup) return;
|
| 545 |
+
const grid = document.querySelector(".palettes .palettes__grid");
|
| 546 |
+
if (!grid) return;
|
| 547 |
+
grid.addEventListener("click", async (e) => {
|
| 548 |
+
const btn = e.target.closest
|
| 549 |
+
? e.target.closest(".palette-card__copy")
|
| 550 |
+
: null;
|
| 551 |
+
if (!btn) return;
|
| 552 |
+
const card = btn.closest(".palette-card");
|
| 553 |
+
if (!card) return;
|
| 554 |
+
const colors = (card.dataset.colors || "").split(",").filter(Boolean);
|
| 555 |
+
const json = JSON.stringify(colors, null, 2);
|
| 556 |
+
try {
|
| 557 |
+
await navigator.clipboard.writeText(json);
|
| 558 |
+
const old = btn.innerHTML;
|
| 559 |
+
btn.innerHTML =
|
| 560 |
+
'<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>';
|
| 561 |
+
setTimeout(() => (btn.innerHTML = old), 900);
|
| 562 |
+
} catch {
|
| 563 |
+
window.prompt("Copy palette", json);
|
| 564 |
+
}
|
| 565 |
+
});
|
| 566 |
+
copyDelegationSetup = true;
|
| 567 |
+
};
|
| 568 |
+
const bootstrap = () => {
|
| 569 |
+
setupCbSim();
|
| 570 |
+
setupCountControl();
|
| 571 |
+
setupCopyDelegation();
|
| 572 |
+
// Render immediately
|
| 573 |
+
render();
|
| 574 |
+
// Re-render on palette updates
|
| 575 |
+
document.addEventListener("palettes:updated", render);
|
| 576 |
+
// Force an immediate notify after listeners are attached (ensures initial render)
|
| 577 |
+
try {
|
| 578 |
+
if (
|
| 579 |
+
window.ColorPalettes &&
|
| 580 |
+
typeof window.ColorPalettes.notify === "function"
|
| 581 |
+
)
|
| 582 |
+
window.ColorPalettes.notify();
|
| 583 |
+
else if (
|
| 584 |
+
window.ColorPalettes &&
|
| 585 |
+
typeof window.ColorPalettes.refresh === "function"
|
| 586 |
+
)
|
| 587 |
+
window.ColorPalettes.refresh();
|
| 588 |
+
} catch {}
|
| 589 |
+
};
|
| 590 |
+
if (document.readyState === "loading") {
|
| 591 |
+
document.addEventListener("DOMContentLoaded", bootstrap, { once: true });
|
| 592 |
+
} else {
|
| 593 |
+
bootstrap();
|
| 594 |
+
}
|
| 595 |
+
})();
|
| 596 |
+
</script>
|
app/src/components/trackio/Trackio.svelte
CHANGED
|
@@ -1,14 +1,20 @@
|
|
| 1 |
<script>
|
| 2 |
-
import * as d3 from
|
| 3 |
-
import { formatAbbrev, smoothMetricData } from
|
| 4 |
-
import {
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
export let normalizeLoss = true;
|
| 13 |
export let logScaleX = false;
|
| 14 |
export let smoothing = false;
|
|
@@ -17,99 +23,143 @@
|
|
| 17 |
let gridEl;
|
| 18 |
let legendItems = [];
|
| 19 |
const cellsDef = [
|
| 20 |
-
{ metric:
|
| 21 |
-
{ metric:
|
| 22 |
-
{ metric:
|
| 23 |
-
{ metric:
|
| 24 |
-
{ metric:
|
| 25 |
];
|
| 26 |
let preparedData = {};
|
| 27 |
let colorsByRun = {};
|
| 28 |
-
|
| 29 |
// Variables for data management (will be initialized in onMount)
|
| 30 |
let dataByMetric = new Map();
|
| 31 |
let metricsToDraw = [];
|
| 32 |
let currentRunList = [];
|
| 33 |
let cycleIdx = 2;
|
| 34 |
-
|
| 35 |
// Dynamic color palette using color-palettes.js helper
|
| 36 |
-
let dynamicPalette = [
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
const updateDynamicPalette = () => {
|
| 39 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
try {
|
| 41 |
-
dynamicPalette = window.ColorPalettes.getColors(
|
|
|
|
|
|
|
|
|
|
| 42 |
} catch (e) {
|
| 43 |
-
console.warn(
|
| 44 |
// Keep fallback palette
|
| 45 |
}
|
| 46 |
}
|
| 47 |
};
|
| 48 |
-
|
| 49 |
const colorForRun = (name) => {
|
| 50 |
const idx = currentRunList.indexOf(name);
|
| 51 |
-
return idx >= 0 ? dynamicPalette[idx % dynamicPalette.length] :
|
| 52 |
};
|
| 53 |
-
|
| 54 |
|
| 55 |
// Jitter function - generates completely new data with new runs
|
| 56 |
-
function jitterData(){
|
| 57 |
-
console.log(
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
// Generate new random data with weighted probability for fewer runs
|
| 60 |
// Higher probability for 2-3 runs, lower for 4-5-6 runs
|
| 61 |
const rand = Math.random();
|
| 62 |
let wantRuns;
|
| 63 |
-
if (rand < 0.4)
|
| 64 |
-
|
| 65 |
-
else if (rand < 0.
|
| 66 |
-
|
| 67 |
-
else
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
// Use realistic ML training step counts
|
| 69 |
const stepsCount = Random.trainingSteps();
|
| 70 |
const runsSim = generateRunNames(wantRuns, stepsCount);
|
| 71 |
-
const steps = Array.from({length: stepsCount}, (_,i)=> i+1);
|
| 72 |
const nextByMetric = new Map();
|
| 73 |
-
const TARGET_METRICS = [
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
// Initialize data structure
|
| 76 |
TARGET_METRICS.forEach((tgt) => {
|
| 77 |
const map = {};
|
| 78 |
-
runsSim.forEach((r) => {
|
|
|
|
|
|
|
| 79 |
nextByMetric.set(tgt, map);
|
| 80 |
});
|
| 81 |
-
|
| 82 |
// Generate curves for each run
|
| 83 |
-
runsSim.forEach(run => {
|
| 84 |
const curves = genCurves(stepsCount);
|
| 85 |
-
steps.forEach((s,i)=>{
|
| 86 |
-
nextByMetric.get(
|
| 87 |
-
nextByMetric
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
nextByMetric
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
});
|
| 92 |
});
|
| 93 |
-
|
| 94 |
// Update all reactive data
|
| 95 |
-
nextByMetric.forEach((v,k)=> dataByMetric.set(k,v));
|
| 96 |
metricsToDraw = TARGET_METRICS;
|
| 97 |
currentRunList = runsSim.slice();
|
| 98 |
updateDynamicPalette(); // Generate new colors based on run count
|
| 99 |
-
legendItems = currentRunList.map((name) => ({
|
|
|
|
|
|
|
|
|
|
| 100 |
updatePreparedData();
|
| 101 |
-
colorsByRun = Object.fromEntries(
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
// Public API: allow external theme switch
|
| 107 |
-
function setTheme(name){
|
| 108 |
-
variant = name ===
|
| 109 |
updateThemeClass();
|
| 110 |
-
|
| 111 |
// Debug log for font application
|
| 112 |
-
if (typeof window !==
|
| 113 |
console.log(`Theme switched to: ${variant}`);
|
| 114 |
if (hostEl) {
|
| 115 |
const computedStyle = getComputedStyle(hostEl);
|
|
@@ -135,41 +185,67 @@
|
|
| 135 |
|
| 136 |
// Public API: generate massive test dataset
|
| 137 |
function generateMassiveDataset(steps = null, runs = 3) {
|
| 138 |
-
console.log(
|
| 139 |
-
|
|
|
|
|
|
|
| 140 |
const result = generateMassiveTestDataset(steps, runs);
|
| 141 |
-
|
| 142 |
// Update reactive data with massive dataset
|
| 143 |
result.dataByMetric.forEach((v, k) => dataByMetric.set(k, v));
|
| 144 |
-
metricsToDraw = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
currentRunList = result.runNames.slice();
|
| 146 |
updateDynamicPalette();
|
| 147 |
-
legendItems = currentRunList.map((name) => ({
|
|
|
|
|
|
|
|
|
|
| 148 |
updatePreparedData();
|
| 149 |
-
colorsByRun = Object.fromEntries(
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
console.log(`📊 Total data points: ${result.totalPoints.toLocaleString()}`);
|
| 153 |
console.log(`🎯 Description: ${result.description}`);
|
| 154 |
-
|
| 155 |
return result;
|
| 156 |
}
|
| 157 |
|
| 158 |
// Public API: add live data point for simulation
|
| 159 |
function addLiveDataPoint(runName, dataPoint) {
|
| 160 |
console.log(`Adding live data point for run "${runName}":`, dataPoint);
|
| 161 |
-
|
| 162 |
// Add run to currentRunList if it doesn't exist
|
| 163 |
if (!currentRunList.includes(runName)) {
|
| 164 |
currentRunList = [...currentRunList, runName];
|
| 165 |
updateDynamicPalette();
|
| 166 |
-
colorsByRun = Object.fromEntries(
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
-
|
| 170 |
// Initialize data structures for the run if needed
|
| 171 |
-
const TARGET_METRICS = [
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
if (!dataByMetric.has(metric)) {
|
| 174 |
dataByMetric.set(metric, {});
|
| 175 |
}
|
|
@@ -178,114 +254,125 @@
|
|
| 178 |
metricData[runName] = [];
|
| 179 |
}
|
| 180 |
});
|
| 181 |
-
|
| 182 |
// Add the new data points to each metric
|
| 183 |
const step = dataPoint.step;
|
| 184 |
-
|
| 185 |
// Add epoch data
|
| 186 |
-
const epochData = dataByMetric.get(
|
| 187 |
epochData[runName].push({ step, value: step });
|
| 188 |
-
|
| 189 |
// Add accuracy data (train and val get the same value for simplicity)
|
| 190 |
if (dataPoint.accuracy !== undefined) {
|
| 191 |
-
const trainAccData = dataByMetric.get(
|
| 192 |
-
const valAccData = dataByMetric.get(
|
| 193 |
-
|
| 194 |
// Add some noise between train and val accuracy
|
| 195 |
const trainAcc = dataPoint.accuracy;
|
| 196 |
-
const valAcc = Math.max(
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
trainAccData[runName].push({ step, value: trainAcc });
|
| 199 |
valAccData[runName].push({ step, value: valAcc });
|
| 200 |
}
|
| 201 |
-
|
| 202 |
// Add loss data (train and val get the same value for simplicity)
|
| 203 |
if (dataPoint.loss !== undefined) {
|
| 204 |
-
const trainLossData = dataByMetric.get(
|
| 205 |
-
const valLossData = dataByMetric.get(
|
| 206 |
-
|
| 207 |
// Add some noise between train and val loss
|
| 208 |
const trainLoss = dataPoint.loss;
|
| 209 |
const valLoss = dataPoint.loss + 0.05 + Math.random() * 0.1;
|
| 210 |
-
|
| 211 |
trainLossData[runName].push({ step, value: trainLoss });
|
| 212 |
valLossData[runName].push({ step, value: valLoss });
|
| 213 |
}
|
| 214 |
-
|
| 215 |
// Update all metrics to draw
|
| 216 |
metricsToDraw = TARGET_METRICS;
|
| 217 |
-
|
| 218 |
// Update prepared data with new values
|
| 219 |
updatePreparedData();
|
| 220 |
-
|
| 221 |
-
console.log(
|
|
|
|
|
|
|
| 222 |
}
|
| 223 |
|
| 224 |
// Update prepared data with optional smoothing
|
| 225 |
let preparedRawData = {}; // Store original data for background display
|
| 226 |
-
|
| 227 |
function updatePreparedData() {
|
| 228 |
-
const TARGET_METRICS = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
let dataToUse = {};
|
| 230 |
let rawDataToStore = {};
|
| 231 |
-
|
| 232 |
-
TARGET_METRICS.forEach(metric => {
|
| 233 |
const rawData = dataByMetric.get(metric);
|
| 234 |
if (rawData) {
|
| 235 |
// Store original data
|
| 236 |
rawDataToStore[metric] = rawData;
|
| 237 |
-
|
| 238 |
// Apply smoothing if enabled (except for epoch which should stay exact)
|
| 239 |
-
dataToUse[metric] =
|
| 240 |
-
|
| 241 |
-
|
|
|
|
| 242 |
}
|
| 243 |
});
|
| 244 |
-
|
| 245 |
preparedData = dataToUse;
|
| 246 |
preparedRawData = rawDataToStore;
|
| 247 |
console.log(`Prepared data updated, smoothing: ${smoothing}`);
|
| 248 |
}
|
| 249 |
|
| 250 |
-
function updateThemeClass(){
|
| 251 |
if (!hostEl) return;
|
| 252 |
-
hostEl.classList.toggle(
|
| 253 |
-
hostEl.classList.toggle(
|
| 254 |
-
hostEl.setAttribute(
|
| 255 |
}
|
| 256 |
|
| 257 |
$: updateThemeClass();
|
| 258 |
|
| 259 |
-
|
| 260 |
// Chart logic now handled by Cell.svelte
|
| 261 |
-
|
| 262 |
// Fullscreen navigation state
|
| 263 |
let currentFullscreenIndex = 0;
|
| 264 |
let isModalOpen = false;
|
| 265 |
-
|
| 266 |
function handleNavigate(newIndex) {
|
| 267 |
currentFullscreenIndex = newIndex;
|
| 268 |
}
|
| 269 |
-
|
| 270 |
function openModal(index) {
|
| 271 |
currentFullscreenIndex = index;
|
| 272 |
isModalOpen = true;
|
| 273 |
}
|
| 274 |
-
|
| 275 |
function closeModal() {
|
| 276 |
isModalOpen = false;
|
| 277 |
}
|
| 278 |
-
|
| 279 |
// Prepare all charts data for navigation
|
| 280 |
-
$: allChartsData = cellsDef.map(c => ({
|
| 281 |
metricKey: c.metric,
|
| 282 |
titleText: c.title,
|
| 283 |
metricData: (preparedData && preparedData[c.metric]) || {},
|
| 284 |
-
rawMetricData: (preparedRawData && preparedRawData[c.metric]) || {}
|
| 285 |
}));
|
| 286 |
-
|
| 287 |
// Color function for the modal
|
| 288 |
-
$: modalColorForRun = (name) => colorsByRun[name] ||
|
| 289 |
|
| 290 |
let cleanup = null;
|
| 291 |
onMount(() => {
|
|
@@ -293,66 +380,97 @@
|
|
| 293 |
hostEl.__setTheme = setTheme;
|
| 294 |
|
| 295 |
// Jitter & Simulate functions
|
| 296 |
-
function rebuildLegend(){
|
| 297 |
updateDynamicPalette(); // Update colors when adding new data
|
| 298 |
-
legendItems = currentRunList.map((name) => ({
|
|
|
|
|
|
|
|
|
|
| 299 |
}
|
| 300 |
-
|
| 301 |
-
function simulateData(){
|
| 302 |
// Generate new random data with weighted probability for fewer runs
|
| 303 |
// Higher probability for 2-3 runs, lower for 4-5-6 runs
|
| 304 |
const rand = Math.random();
|
| 305 |
let wantRuns;
|
| 306 |
-
if (rand < 0.4)
|
| 307 |
-
|
| 308 |
-
else if (rand < 0.
|
| 309 |
-
|
| 310 |
-
else
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
// Use realistic ML training step counts with cycling scenarios
|
| 312 |
let stepsCount;
|
| 313 |
if (cycleIdx === 0) {
|
| 314 |
-
stepsCount = Random.trainingStepsForScenario(
|
| 315 |
} else if (cycleIdx === 1) {
|
| 316 |
-
stepsCount = Random.trainingStepsForScenario(
|
| 317 |
} else if (cycleIdx === 2) {
|
| 318 |
-
stepsCount = Random.trainingStepsForScenario(
|
| 319 |
} else if (cycleIdx === 3) {
|
| 320 |
-
stepsCount = Random.trainingStepsForScenario(
|
| 321 |
} else if (cycleIdx === 4) {
|
| 322 |
-
stepsCount = Random.trainingStepsForScenario(
|
| 323 |
} else if (cycleIdx === 5) {
|
| 324 |
-
stepsCount = Random.trainingStepsForScenario(
|
| 325 |
} else {
|
| 326 |
stepsCount = Random.trainingSteps(); // Full range for variety
|
| 327 |
}
|
| 328 |
cycleIdx = (cycleIdx + 1) % 7; // Cycle through 7 scenarios now
|
| 329 |
-
|
| 330 |
const runsSim = generateRunNames(wantRuns, stepsCount);
|
| 331 |
-
const steps = Array.from({length: stepsCount}, (_,i)=> i+1);
|
| 332 |
const nextByMetric = new Map();
|
| 333 |
-
const TARGET_METRICS = [
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
mList.forEach((tgt) => {
|
| 336 |
const map = {};
|
| 337 |
-
runsSim.forEach((r) => {
|
|
|
|
|
|
|
| 338 |
nextByMetric.set(tgt, map);
|
| 339 |
});
|
| 340 |
-
runsSim.forEach(run => {
|
| 341 |
const curves = genCurves(stepsCount);
|
| 342 |
-
steps.forEach((s,i)=>{
|
| 343 |
-
if (mList.includes(
|
| 344 |
-
|
| 345 |
-
if (mList.includes(
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
});
|
| 349 |
});
|
| 350 |
-
nextByMetric.forEach((v,k)=> dataByMetric.set(k,v));
|
| 351 |
currentRunList = runsSim.slice();
|
| 352 |
rebuildLegend();
|
| 353 |
updatePreparedData();
|
| 354 |
updateDynamicPalette(); // Update colors when rebuilding
|
| 355 |
-
colorsByRun = Object.fromEntries(
|
|
|
|
|
|
|
| 356 |
}
|
| 357 |
// No need for event listeners anymore - we'll use reactive statement
|
| 358 |
|
|
@@ -360,105 +478,158 @@
|
|
| 360 |
simulateData();
|
| 361 |
// Svelte Cells will react to preparedData/colorsByRun updates
|
| 362 |
|
| 363 |
-
cleanup = () => {
|
| 364 |
// No cleanup needed for reactive statements
|
| 365 |
};
|
| 366 |
});
|
| 367 |
|
| 368 |
-
onDestroy(() => {
|
|
|
|
|
|
|
| 369 |
|
| 370 |
// Expose instance for debugging and external theme control
|
| 371 |
onMount(() => {
|
| 372 |
-
window.trackioInstance = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
if (hostEl) {
|
| 374 |
-
hostEl.__trackioInstance = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
}
|
| 376 |
-
|
| 377 |
// Initialize dynamic palette
|
| 378 |
updateDynamicPalette();
|
| 379 |
-
|
| 380 |
// Listen for palette updates from color-palettes.js
|
| 381 |
const handlePaletteUpdate = () => {
|
| 382 |
updateDynamicPalette();
|
| 383 |
// Rebuild legend and colors if needed
|
| 384 |
if (currentRunList.length > 0) {
|
| 385 |
-
legendItems = currentRunList.map((name) => ({
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
}
|
| 388 |
};
|
| 389 |
-
|
| 390 |
-
document.addEventListener(
|
| 391 |
-
|
| 392 |
// Cleanup listener on destroy
|
| 393 |
return () => {
|
| 394 |
-
document.removeEventListener(
|
| 395 |
};
|
| 396 |
});
|
| 397 |
|
| 398 |
// React to jitter trigger from store
|
| 399 |
$: {
|
| 400 |
-
console.log(
|
|
|
|
|
|
|
|
|
|
| 401 |
if ($jitterTrigger > 0) {
|
| 402 |
-
console.log(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
jitterData();
|
| 404 |
}
|
| 405 |
}
|
| 406 |
|
| 407 |
// Legend ghost helpers (hover effects)
|
| 408 |
-
function ghostRun(run){
|
| 409 |
try {
|
| 410 |
-
hostEl.classList.add(
|
| 411 |
-
|
| 412 |
// Ghost the chart lines and points
|
| 413 |
-
hostEl.querySelectorAll(
|
| 414 |
-
cell
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
});
|
| 418 |
-
|
| 419 |
// Ghost the legend items
|
| 420 |
-
hostEl.querySelectorAll(
|
| 421 |
-
const itemRun = item.getAttribute(
|
| 422 |
-
item.classList.toggle(
|
| 423 |
});
|
| 424 |
-
} catch(_) {}
|
| 425 |
}
|
| 426 |
-
function clearGhost(){
|
| 427 |
try {
|
| 428 |
-
hostEl.classList.remove(
|
| 429 |
-
|
| 430 |
// Clear ghost from chart lines and points
|
| 431 |
-
hostEl.querySelectorAll(
|
| 432 |
-
cell
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
});
|
| 436 |
-
|
| 437 |
// Clear ghost from legend items
|
| 438 |
-
hostEl.querySelectorAll(
|
| 439 |
-
item.classList.remove(
|
| 440 |
});
|
| 441 |
-
} catch(_) {}
|
| 442 |
}
|
| 443 |
</script>
|
| 444 |
|
| 445 |
<div class="trackio theme--classic" bind:this={hostEl} data-variant={variant}>
|
| 446 |
<div class="trackio__header">
|
| 447 |
-
<Legend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
</div>
|
| 449 |
<div class="trackio__grid" bind:this={gridEl}>
|
| 450 |
{#each cellsDef as c, i}
|
| 451 |
-
<Cell
|
| 452 |
-
metricKey={c.metric}
|
| 453 |
-
titleText={c.title}
|
| 454 |
-
wide={c.wide}
|
| 455 |
-
{variant}
|
| 456 |
-
{normalizeLoss}
|
| 457 |
-
{logScaleX}
|
| 458 |
-
{smoothing}
|
| 459 |
-
metricData={(preparedData && preparedData[c.metric]) || {}}
|
| 460 |
-
rawMetricData={(preparedRawData && preparedRawData[c.metric]) || {}}
|
| 461 |
-
colorForRun={(name)=> colorsByRun[name] ||
|
| 462 |
{hostEl}
|
| 463 |
currentIndex={i}
|
| 464 |
onOpenModal={openModal}
|
|
@@ -467,9 +638,17 @@
|
|
| 467 |
</div>
|
| 468 |
<div class="trackio__footer">
|
| 469 |
<small>
|
| 470 |
-
Built with <a
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
<span class="separator">•</span>
|
| 472 |
-
<a
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
</small>
|
| 474 |
</div>
|
| 475 |
</div>
|
|
@@ -477,7 +656,7 @@
|
|
| 477 |
<!-- Centralized Fullscreen Modal -->
|
| 478 |
<FullscreenModal
|
| 479 |
visible={isModalOpen}
|
| 480 |
-
title={allChartsData[currentFullscreenIndex]?.titleText ||
|
| 481 |
metricData={allChartsData[currentFullscreenIndex]?.metricData || {}}
|
| 482 |
rawMetricData={allChartsData[currentFullscreenIndex]?.rawMetricData || {}}
|
| 483 |
colorForRun={modalColorForRun}
|
|
@@ -485,8 +664,8 @@
|
|
| 485 |
{logScaleX}
|
| 486 |
{smoothing}
|
| 487 |
{normalizeLoss}
|
| 488 |
-
metricKey={allChartsData[currentFullscreenIndex]?.metricKey ||
|
| 489 |
-
titleText={allChartsData[currentFullscreenIndex]?.titleText ||
|
| 490 |
currentIndex={currentFullscreenIndex}
|
| 491 |
totalCharts={cellsDef.length}
|
| 492 |
onNavigate={handleNavigate}
|
|
@@ -499,12 +678,13 @@
|
|
| 499 |
========================= */
|
| 500 |
|
| 501 |
/* Font imports for themes - ensure Roboto Mono is loaded for Oblivion theme */
|
| 502 |
-
@import url(
|
| 503 |
-
|
| 504 |
/* Fallback font-face declaration */
|
| 505 |
@font-face {
|
| 506 |
-
font-family:
|
| 507 |
-
src: url(
|
|
|
|
| 508 |
font-weight: 400;
|
| 509 |
font-style: normal;
|
| 510 |
font-display: swap;
|
|
@@ -515,31 +695,37 @@
|
|
| 515 |
position: relative;
|
| 516 |
--z-tooltip: 50;
|
| 517 |
--z-overlay: 99999999;
|
| 518 |
-
|
| 519 |
/* Typography */
|
| 520 |
-
--trackio-font-family: var(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
--trackio-font-weight-normal: 400;
|
| 522 |
--trackio-font-weight-medium: 600;
|
| 523 |
--trackio-font-weight-bold: 700;
|
| 524 |
-
|
| 525 |
/* Apply font-family to root element */
|
| 526 |
font-family: var(--trackio-font-family);
|
| 527 |
-
|
| 528 |
/* Base color system for Classic theme */
|
| 529 |
--trackio-base: #323232;
|
| 530 |
--trackio-primary: var(--trackio-base);
|
| 531 |
--trackio-dim: color-mix(in srgb, var(--trackio-base) 28%, transparent);
|
| 532 |
--trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
|
| 533 |
--trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
|
| 534 |
-
|
| 535 |
/* Chart rendering */
|
| 536 |
-
--trackio-chart-grid-type:
|
| 537 |
--trackio-chart-axis-stroke: var(--trackio-dim);
|
| 538 |
--trackio-chart-axis-text: var(--trackio-text);
|
| 539 |
--trackio-chart-grid-stroke: var(--trackio-subtle);
|
| 540 |
--trackio-chart-grid-opacity: 1;
|
| 541 |
}
|
| 542 |
-
|
| 543 |
/* Dark mode overrides for Classic theme */
|
| 544 |
:global([data-theme="dark"]) .trackio.theme--classic {
|
| 545 |
--trackio-base: #ffffff;
|
|
@@ -547,28 +733,28 @@
|
|
| 547 |
--trackio-dim: color-mix(in srgb, var(--trackio-base) 25%, transparent);
|
| 548 |
--trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
|
| 549 |
--trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
|
| 550 |
-
|
| 551 |
/* Cell background for dark mode */
|
| 552 |
--trackio-cell-background: rgba(255, 255, 255, 0.03);
|
| 553 |
}
|
| 554 |
-
|
| 555 |
.trackio.theme--classic {
|
| 556 |
/* Cell styling */
|
| 557 |
--trackio-cell-background: rgba(0, 0, 0, 0.02);
|
| 558 |
--trackio-cell-border: var(--border-color, rgba(0, 0, 0, 0.1));
|
| 559 |
--trackio-cell-corner-inset: 0px;
|
| 560 |
--trackio-cell-gap: 12px;
|
| 561 |
-
|
| 562 |
/* Typography */
|
| 563 |
--trackio-text-primary: var(--text-color, rgba(0, 0, 0, 0.9));
|
| 564 |
--trackio-text-secondary: var(--muted-color, rgba(0, 0, 0, 0.6));
|
| 565 |
-
--trackio-text-accent: var(--primary-color
|
| 566 |
-
|
| 567 |
/* Tooltip */
|
| 568 |
--trackio-tooltip-background: var(--surface-bg, white);
|
| 569 |
--trackio-tooltip-border: var(--border-color, rgba(0, 0, 0, 0.1));
|
| 570 |
--trackio-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
| 571 |
-
|
| 572 |
/* Legend */
|
| 573 |
--trackio-legend-text: var(--text-color, rgba(0, 0, 0, 0.9));
|
| 574 |
--trackio-legend-swatch-border: var(--border-color, rgba(0, 0, 0, 0.1));
|
|
@@ -577,14 +763,14 @@
|
|
| 577 |
/* Dark mode adjustments */
|
| 578 |
:global([data-theme="dark"]) .trackio {
|
| 579 |
--trackio-chart-axis-stroke: rgba(255, 255, 255, 0.18);
|
| 580 |
-
--trackio-chart-axis-text: rgba(255, 255, 255, 0.
|
| 581 |
--trackio-chart-grid-stroke: rgba(255, 255, 255, 0.08);
|
| 582 |
}
|
| 583 |
|
| 584 |
/* =========================
|
| 585 |
THEME: CLASSIC (Default)
|
| 586 |
========================= */
|
| 587 |
-
|
| 588 |
.trackio.theme--classic {
|
| 589 |
/* Keep default values - no overrides needed */
|
| 590 |
}
|
|
@@ -592,64 +778,96 @@
|
|
| 592 |
/* =========================
|
| 593 |
THEME: OBLIVION
|
| 594 |
========================= */
|
| 595 |
-
|
| 596 |
.trackio.theme--oblivion {
|
| 597 |
/* Core oblivion color system - Light mode: darker colors for visibility */
|
| 598 |
--trackio-oblivion-base: #2a2a2a;
|
| 599 |
--trackio-oblivion-primary: var(--trackio-oblivion-base);
|
| 600 |
-
--trackio-oblivion-dim: color-mix(
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
/* Chart rendering overrides */
|
| 605 |
-
--trackio-chart-grid-type:
|
| 606 |
--trackio-chart-axis-stroke: var(--trackio-oblivion-dim);
|
| 607 |
--trackio-chart-axis-text: var(--trackio-oblivion-primary);
|
| 608 |
--trackio-chart-grid-stroke: var(--trackio-oblivion-dim);
|
| 609 |
--trackio-chart-grid-opacity: 0.6;
|
| 610 |
}
|
| 611 |
-
|
| 612 |
/* Dark mode overrides for Oblivion theme */
|
| 613 |
:global([data-theme="dark"]) .trackio.theme--oblivion {
|
| 614 |
--trackio-oblivion-base: #ffffff;
|
| 615 |
--trackio-oblivion-primary: var(--trackio-oblivion-base);
|
| 616 |
-
--trackio-oblivion-dim: color-mix(
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
}
|
| 620 |
-
|
| 621 |
.trackio.theme--oblivion {
|
| 622 |
/* Cell styling overrides */
|
| 623 |
--trackio-cell-background: var(--trackio-oblivion-subtle);
|
| 624 |
--trackio-cell-border: var(--trackio-oblivion-dim);
|
| 625 |
--trackio-cell-corner-inset: 6px;
|
| 626 |
--trackio-cell-gap: 0px;
|
| 627 |
-
|
| 628 |
/* HUD-specific variables */
|
| 629 |
--trackio-oblivion-hud-gap: 10px;
|
| 630 |
--trackio-oblivion-hud-corner-size: 8px;
|
| 631 |
-
--trackio-oblivion-hud-bg-gradient:
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
/* Typography overrides */
|
| 636 |
--trackio-text-primary: var(--trackio-oblivion-primary);
|
| 637 |
--trackio-text-secondary: var(--trackio-oblivion-dim);
|
| 638 |
--trackio-text-accent: var(--trackio-oblivion-primary);
|
| 639 |
-
|
| 640 |
/* Tooltip overrides */
|
| 641 |
--trackio-tooltip-background: var(--trackio-oblivion-subtle);
|
| 642 |
--trackio-tooltip-border: var(--trackio-oblivion-dim);
|
| 643 |
-
--trackio-tooltip-shadow:
|
| 644 |
-
|
| 645 |
0 2px 8px color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent);
|
| 646 |
-
|
| 647 |
/* Legend overrides */
|
| 648 |
--trackio-legend-text: var(--trackio-oblivion-primary);
|
| 649 |
--trackio-legend-swatch-border: var(--trackio-oblivion-dim);
|
| 650 |
-
|
| 651 |
/* Font styling overrides */
|
| 652 |
-
--trackio-font-family:
|
|
|
|
| 653 |
font-family: var(--trackio-font-family) !important;
|
| 654 |
color: var(--trackio-text-primary);
|
| 655 |
}
|
|
@@ -657,29 +875,42 @@
|
|
| 657 |
/* Force Roboto Mono application in Oblivion theme */
|
| 658 |
.trackio.theme--oblivion,
|
| 659 |
.trackio.theme--oblivion * {
|
| 660 |
-
font-family:
|
|
|
|
| 661 |
}
|
| 662 |
-
|
| 663 |
/* Specific overrides for different elements in Oblivion */
|
| 664 |
.trackio.theme--oblivion .cell-title,
|
| 665 |
.trackio.theme--oblivion .legend-bottom,
|
| 666 |
.trackio.theme--oblivion .legend-title,
|
| 667 |
.trackio.theme--oblivion .item {
|
| 668 |
-
font-family:
|
|
|
|
| 669 |
}
|
| 670 |
|
| 671 |
/* Dark mode adjustments for Oblivion */
|
| 672 |
:global([data-theme="dark"]) .trackio.theme--oblivion {
|
| 673 |
--trackio-oblivion-base: #ffffff;
|
| 674 |
-
--trackio-oblivion-hud-bg-gradient:
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
0 2px 8px color-mix(in srgb, black 10%, transparent);
|
| 682 |
-
|
| 683 |
background: #0f1115;
|
| 684 |
}
|
| 685 |
|
|
@@ -692,9 +923,11 @@
|
|
| 692 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 693 |
gap: var(--trackio-cell-gap);
|
| 694 |
}
|
| 695 |
-
|
| 696 |
@media (max-width: 980px) {
|
| 697 |
-
.trackio__grid {
|
|
|
|
|
|
|
| 698 |
}
|
| 699 |
|
| 700 |
.trackio__header {
|
|
@@ -712,28 +945,37 @@
|
|
| 712 |
.trackio .axes line {
|
| 713 |
stroke: var(--trackio-chart-axis-stroke);
|
| 714 |
}
|
| 715 |
-
|
| 716 |
.trackio .axes text {
|
| 717 |
fill: var(--trackio-chart-axis-text);
|
| 718 |
font-family: var(--trackio-font-family);
|
| 719 |
}
|
| 720 |
-
|
| 721 |
/* Force font-family for SVG text in Oblivion */
|
| 722 |
.trackio.theme--oblivion .axes text {
|
| 723 |
-
font-family:
|
|
|
|
| 724 |
}
|
| 725 |
-
|
| 726 |
.trackio .grid line {
|
| 727 |
stroke: var(--trackio-chart-grid-stroke);
|
| 728 |
opacity: var(--trackio-chart-grid-opacity);
|
| 729 |
}
|
| 730 |
|
| 731 |
/* Grid type switching */
|
| 732 |
-
.trackio .grid-dots {
|
| 733 |
-
|
| 734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
.trackio.theme--oblivion .cell-bg,
|
| 736 |
-
.trackio.theme--oblivion .cell-corners {
|
|
|
|
|
|
|
| 737 |
|
| 738 |
/* =========================
|
| 739 |
FOOTER
|
|
@@ -777,8 +1019,7 @@
|
|
| 777 |
}
|
| 778 |
|
| 779 |
.trackio.theme--oblivion .trackio__footer small {
|
| 780 |
-
font-family:
|
|
|
|
| 781 |
}
|
| 782 |
</style>
|
| 783 |
-
|
| 784 |
-
|
|
|
|
| 1 |
<script>
|
| 2 |
+
import * as d3 from "d3";
|
| 3 |
+
import { formatAbbrev, smoothMetricData } from "./core/chart-utils.js";
|
| 4 |
+
import {
|
| 5 |
+
generateRunNames,
|
| 6 |
+
genCurves,
|
| 7 |
+
Random,
|
| 8 |
+
Performance,
|
| 9 |
+
generateMassiveTestDataset,
|
| 10 |
+
} from "./core/data-generator.js";
|
| 11 |
+
import Legend from "./components/Legend.svelte";
|
| 12 |
+
import Cell from "./components/Cell.svelte";
|
| 13 |
+
import FullscreenModal from "./components/FullscreenModal.svelte";
|
| 14 |
+
import { onMount, onDestroy } from "svelte";
|
| 15 |
+
import { jitterTrigger } from "./core/store.js";
|
| 16 |
+
|
| 17 |
+
export let variant = "classic"; // 'classic' | 'oblivion'
|
| 18 |
export let normalizeLoss = true;
|
| 19 |
export let logScaleX = false;
|
| 20 |
export let smoothing = false;
|
|
|
|
| 23 |
let gridEl;
|
| 24 |
let legendItems = [];
|
| 25 |
const cellsDef = [
|
| 26 |
+
{ metric: "epoch", title: "Epoch" },
|
| 27 |
+
{ metric: "train_accuracy", title: "Train accuracy" },
|
| 28 |
+
{ metric: "train_loss", title: "Train loss" },
|
| 29 |
+
{ metric: "val_accuracy", title: "Val accuracy" },
|
| 30 |
+
{ metric: "val_loss", title: "Val loss", wide: true },
|
| 31 |
];
|
| 32 |
let preparedData = {};
|
| 33 |
let colorsByRun = {};
|
| 34 |
+
|
| 35 |
// Variables for data management (will be initialized in onMount)
|
| 36 |
let dataByMetric = new Map();
|
| 37 |
let metricsToDraw = [];
|
| 38 |
let currentRunList = [];
|
| 39 |
let cycleIdx = 2;
|
| 40 |
+
|
| 41 |
// Dynamic color palette using color-palettes.js helper
|
| 42 |
+
let dynamicPalette = [
|
| 43 |
+
"#0ea5e9",
|
| 44 |
+
"#8b5cf6",
|
| 45 |
+
"#f59e0b",
|
| 46 |
+
"#ef4444",
|
| 47 |
+
"#10b981",
|
| 48 |
+
"#f97316",
|
| 49 |
+
"#3b82f6",
|
| 50 |
+
"#8b5ad6",
|
| 51 |
+
]; // fallback
|
| 52 |
+
|
| 53 |
const updateDynamicPalette = () => {
|
| 54 |
+
if (
|
| 55 |
+
typeof window !== "undefined" &&
|
| 56 |
+
window.ColorPalettes &&
|
| 57 |
+
currentRunList.length > 0
|
| 58 |
+
) {
|
| 59 |
try {
|
| 60 |
+
dynamicPalette = window.ColorPalettes.getColors(
|
| 61 |
+
"categorical",
|
| 62 |
+
currentRunList.length,
|
| 63 |
+
);
|
| 64 |
} catch (e) {
|
| 65 |
+
console.warn("Failed to generate dynamic palette:", e);
|
| 66 |
// Keep fallback palette
|
| 67 |
}
|
| 68 |
}
|
| 69 |
};
|
| 70 |
+
|
| 71 |
const colorForRun = (name) => {
|
| 72 |
const idx = currentRunList.indexOf(name);
|
| 73 |
+
return idx >= 0 ? dynamicPalette[idx % dynamicPalette.length] : "#999";
|
| 74 |
};
|
|
|
|
| 75 |
|
| 76 |
// Jitter function - generates completely new data with new runs
|
| 77 |
+
function jitterData() {
|
| 78 |
+
console.log(
|
| 79 |
+
"jitterData called - generating new data with random number of runs",
|
| 80 |
+
); // Debug log
|
| 81 |
+
|
| 82 |
// Generate new random data with weighted probability for fewer runs
|
| 83 |
// Higher probability for 2-3 runs, lower for 4-5-6 runs
|
| 84 |
const rand = Math.random();
|
| 85 |
let wantRuns;
|
| 86 |
+
if (rand < 0.4)
|
| 87 |
+
wantRuns = 2; // 40% chance
|
| 88 |
+
else if (rand < 0.7)
|
| 89 |
+
wantRuns = 3; // 30% chance
|
| 90 |
+
else if (rand < 0.85)
|
| 91 |
+
wantRuns = 4; // 15% chance
|
| 92 |
+
else if (rand < 0.95)
|
| 93 |
+
wantRuns = 5; // 10% chance
|
| 94 |
+
else wantRuns = 6; // 5% chance
|
| 95 |
// Use realistic ML training step counts
|
| 96 |
const stepsCount = Random.trainingSteps();
|
| 97 |
const runsSim = generateRunNames(wantRuns, stepsCount);
|
| 98 |
+
const steps = Array.from({ length: stepsCount }, (_, i) => i + 1);
|
| 99 |
const nextByMetric = new Map();
|
| 100 |
+
const TARGET_METRICS = [
|
| 101 |
+
"epoch",
|
| 102 |
+
"train_accuracy",
|
| 103 |
+
"train_loss",
|
| 104 |
+
"val_accuracy",
|
| 105 |
+
"val_loss",
|
| 106 |
+
];
|
| 107 |
+
|
| 108 |
// Initialize data structure
|
| 109 |
TARGET_METRICS.forEach((tgt) => {
|
| 110 |
const map = {};
|
| 111 |
+
runsSim.forEach((r) => {
|
| 112 |
+
map[r] = [];
|
| 113 |
+
});
|
| 114 |
nextByMetric.set(tgt, map);
|
| 115 |
});
|
| 116 |
+
|
| 117 |
// Generate curves for each run
|
| 118 |
+
runsSim.forEach((run) => {
|
| 119 |
const curves = genCurves(stepsCount);
|
| 120 |
+
steps.forEach((s, i) => {
|
| 121 |
+
nextByMetric.get("epoch")[run].push({ step: s, value: s });
|
| 122 |
+
nextByMetric
|
| 123 |
+
.get("train_accuracy")
|
| 124 |
+
[run].push({ step: s, value: curves.accTrain[i] });
|
| 125 |
+
nextByMetric
|
| 126 |
+
.get("val_accuracy")
|
| 127 |
+
[run].push({ step: s, value: curves.accVal[i] });
|
| 128 |
+
nextByMetric
|
| 129 |
+
.get("train_loss")
|
| 130 |
+
[run].push({ step: s, value: curves.lossTrain[i] });
|
| 131 |
+
nextByMetric
|
| 132 |
+
.get("val_loss")
|
| 133 |
+
[run].push({ step: s, value: curves.lossVal[i] });
|
| 134 |
});
|
| 135 |
});
|
| 136 |
+
|
| 137 |
// Update all reactive data
|
| 138 |
+
nextByMetric.forEach((v, k) => dataByMetric.set(k, v));
|
| 139 |
metricsToDraw = TARGET_METRICS;
|
| 140 |
currentRunList = runsSim.slice();
|
| 141 |
updateDynamicPalette(); // Generate new colors based on run count
|
| 142 |
+
legendItems = currentRunList.map((name) => ({
|
| 143 |
+
name,
|
| 144 |
+
color: colorForRun(name),
|
| 145 |
+
}));
|
| 146 |
updatePreparedData();
|
| 147 |
+
colorsByRun = Object.fromEntries(
|
| 148 |
+
currentRunList.map((name) => [name, colorForRun(name)]),
|
| 149 |
+
);
|
| 150 |
+
|
| 151 |
+
console.log(
|
| 152 |
+
`jitterData completed - generated ${wantRuns} runs with ${stepsCount} steps`,
|
| 153 |
+
); // Debug log
|
| 154 |
}
|
| 155 |
|
| 156 |
// Public API: allow external theme switch
|
| 157 |
+
function setTheme(name) {
|
| 158 |
+
variant = name === "oblivion" ? "oblivion" : "classic";
|
| 159 |
updateThemeClass();
|
| 160 |
+
|
| 161 |
// Debug log for font application
|
| 162 |
+
if (typeof window !== "undefined") {
|
| 163 |
console.log(`Theme switched to: ${variant}`);
|
| 164 |
if (hostEl) {
|
| 165 |
const computedStyle = getComputedStyle(hostEl);
|
|
|
|
| 185 |
|
| 186 |
// Public API: generate massive test dataset
|
| 187 |
function generateMassiveDataset(steps = null, runs = 3) {
|
| 188 |
+
console.log(
|
| 189 |
+
"🧪 Generating massive test dataset for sampling validation...",
|
| 190 |
+
);
|
| 191 |
+
|
| 192 |
const result = generateMassiveTestDataset(steps, runs);
|
| 193 |
+
|
| 194 |
// Update reactive data with massive dataset
|
| 195 |
result.dataByMetric.forEach((v, k) => dataByMetric.set(k, v));
|
| 196 |
+
metricsToDraw = [
|
| 197 |
+
"epoch",
|
| 198 |
+
"train_accuracy",
|
| 199 |
+
"train_loss",
|
| 200 |
+
"val_accuracy",
|
| 201 |
+
"val_loss",
|
| 202 |
+
];
|
| 203 |
currentRunList = result.runNames.slice();
|
| 204 |
updateDynamicPalette();
|
| 205 |
+
legendItems = currentRunList.map((name) => ({
|
| 206 |
+
name,
|
| 207 |
+
color: colorForRun(name),
|
| 208 |
+
}));
|
| 209 |
updatePreparedData();
|
| 210 |
+
colorsByRun = Object.fromEntries(
|
| 211 |
+
currentRunList.map((name) => [name, colorForRun(name)]),
|
| 212 |
+
);
|
| 213 |
+
|
| 214 |
+
console.log(
|
| 215 |
+
`✅ Massive dataset loaded: ${result.stepCount} steps × ${result.runNames.length} runs`,
|
| 216 |
+
);
|
| 217 |
console.log(`📊 Total data points: ${result.totalPoints.toLocaleString()}`);
|
| 218 |
console.log(`🎯 Description: ${result.description}`);
|
| 219 |
+
|
| 220 |
return result;
|
| 221 |
}
|
| 222 |
|
| 223 |
// Public API: add live data point for simulation
|
| 224 |
function addLiveDataPoint(runName, dataPoint) {
|
| 225 |
console.log(`Adding live data point for run "${runName}":`, dataPoint);
|
| 226 |
+
|
| 227 |
// Add run to currentRunList if it doesn't exist
|
| 228 |
if (!currentRunList.includes(runName)) {
|
| 229 |
currentRunList = [...currentRunList, runName];
|
| 230 |
updateDynamicPalette();
|
| 231 |
+
colorsByRun = Object.fromEntries(
|
| 232 |
+
currentRunList.map((name) => [name, colorForRun(name)]),
|
| 233 |
+
);
|
| 234 |
+
legendItems = currentRunList.map((name) => ({
|
| 235 |
+
name,
|
| 236 |
+
color: colorForRun(name),
|
| 237 |
+
}));
|
| 238 |
}
|
| 239 |
+
|
| 240 |
// Initialize data structures for the run if needed
|
| 241 |
+
const TARGET_METRICS = [
|
| 242 |
+
"epoch",
|
| 243 |
+
"train_accuracy",
|
| 244 |
+
"train_loss",
|
| 245 |
+
"val_accuracy",
|
| 246 |
+
"val_loss",
|
| 247 |
+
];
|
| 248 |
+
TARGET_METRICS.forEach((metric) => {
|
| 249 |
if (!dataByMetric.has(metric)) {
|
| 250 |
dataByMetric.set(metric, {});
|
| 251 |
}
|
|
|
|
| 254 |
metricData[runName] = [];
|
| 255 |
}
|
| 256 |
});
|
| 257 |
+
|
| 258 |
// Add the new data points to each metric
|
| 259 |
const step = dataPoint.step;
|
| 260 |
+
|
| 261 |
// Add epoch data
|
| 262 |
+
const epochData = dataByMetric.get("epoch");
|
| 263 |
epochData[runName].push({ step, value: step });
|
| 264 |
+
|
| 265 |
// Add accuracy data (train and val get the same value for simplicity)
|
| 266 |
if (dataPoint.accuracy !== undefined) {
|
| 267 |
+
const trainAccData = dataByMetric.get("train_accuracy");
|
| 268 |
+
const valAccData = dataByMetric.get("val_accuracy");
|
| 269 |
+
|
| 270 |
// Add some noise between train and val accuracy
|
| 271 |
const trainAcc = dataPoint.accuracy;
|
| 272 |
+
const valAcc = Math.max(
|
| 273 |
+
0,
|
| 274 |
+
Math.min(1, dataPoint.accuracy - 0.01 - Math.random() * 0.03),
|
| 275 |
+
);
|
| 276 |
+
|
| 277 |
trainAccData[runName].push({ step, value: trainAcc });
|
| 278 |
valAccData[runName].push({ step, value: valAcc });
|
| 279 |
}
|
| 280 |
+
|
| 281 |
// Add loss data (train and val get the same value for simplicity)
|
| 282 |
if (dataPoint.loss !== undefined) {
|
| 283 |
+
const trainLossData = dataByMetric.get("train_loss");
|
| 284 |
+
const valLossData = dataByMetric.get("val_loss");
|
| 285 |
+
|
| 286 |
// Add some noise between train and val loss
|
| 287 |
const trainLoss = dataPoint.loss;
|
| 288 |
const valLoss = dataPoint.loss + 0.05 + Math.random() * 0.1;
|
| 289 |
+
|
| 290 |
trainLossData[runName].push({ step, value: trainLoss });
|
| 291 |
valLossData[runName].push({ step, value: valLoss });
|
| 292 |
}
|
| 293 |
+
|
| 294 |
// Update all metrics to draw
|
| 295 |
metricsToDraw = TARGET_METRICS;
|
| 296 |
+
|
| 297 |
// Update prepared data with new values
|
| 298 |
updatePreparedData();
|
| 299 |
+
|
| 300 |
+
console.log(
|
| 301 |
+
`Live data point added successfully. Total runs: ${currentRunList.length}`,
|
| 302 |
+
);
|
| 303 |
}
|
| 304 |
|
| 305 |
// Update prepared data with optional smoothing
|
| 306 |
let preparedRawData = {}; // Store original data for background display
|
| 307 |
+
|
| 308 |
function updatePreparedData() {
|
| 309 |
+
const TARGET_METRICS = [
|
| 310 |
+
"epoch",
|
| 311 |
+
"train_accuracy",
|
| 312 |
+
"train_loss",
|
| 313 |
+
"val_accuracy",
|
| 314 |
+
"val_loss",
|
| 315 |
+
];
|
| 316 |
let dataToUse = {};
|
| 317 |
let rawDataToStore = {};
|
| 318 |
+
|
| 319 |
+
TARGET_METRICS.forEach((metric) => {
|
| 320 |
const rawData = dataByMetric.get(metric);
|
| 321 |
if (rawData) {
|
| 322 |
// Store original data
|
| 323 |
rawDataToStore[metric] = rawData;
|
| 324 |
+
|
| 325 |
// Apply smoothing if enabled (except for epoch which should stay exact)
|
| 326 |
+
dataToUse[metric] =
|
| 327 |
+
smoothing && metric !== "epoch"
|
| 328 |
+
? smoothMetricData(rawData, 5) // Window size of 5
|
| 329 |
+
: rawData;
|
| 330 |
}
|
| 331 |
});
|
| 332 |
+
|
| 333 |
preparedData = dataToUse;
|
| 334 |
preparedRawData = rawDataToStore;
|
| 335 |
console.log(`Prepared data updated, smoothing: ${smoothing}`);
|
| 336 |
}
|
| 337 |
|
| 338 |
+
function updateThemeClass() {
|
| 339 |
if (!hostEl) return;
|
| 340 |
+
hostEl.classList.toggle("theme--classic", variant === "classic");
|
| 341 |
+
hostEl.classList.toggle("theme--oblivion", variant === "oblivion");
|
| 342 |
+
hostEl.setAttribute("data-variant", variant);
|
| 343 |
}
|
| 344 |
|
| 345 |
$: updateThemeClass();
|
| 346 |
|
|
|
|
| 347 |
// Chart logic now handled by Cell.svelte
|
| 348 |
+
|
| 349 |
// Fullscreen navigation state
|
| 350 |
let currentFullscreenIndex = 0;
|
| 351 |
let isModalOpen = false;
|
| 352 |
+
|
| 353 |
function handleNavigate(newIndex) {
|
| 354 |
currentFullscreenIndex = newIndex;
|
| 355 |
}
|
| 356 |
+
|
| 357 |
function openModal(index) {
|
| 358 |
currentFullscreenIndex = index;
|
| 359 |
isModalOpen = true;
|
| 360 |
}
|
| 361 |
+
|
| 362 |
function closeModal() {
|
| 363 |
isModalOpen = false;
|
| 364 |
}
|
| 365 |
+
|
| 366 |
// Prepare all charts data for navigation
|
| 367 |
+
$: allChartsData = cellsDef.map((c) => ({
|
| 368 |
metricKey: c.metric,
|
| 369 |
titleText: c.title,
|
| 370 |
metricData: (preparedData && preparedData[c.metric]) || {},
|
| 371 |
+
rawMetricData: (preparedRawData && preparedRawData[c.metric]) || {},
|
| 372 |
}));
|
| 373 |
+
|
| 374 |
// Color function for the modal
|
| 375 |
+
$: modalColorForRun = (name) => colorsByRun[name] || "#999";
|
| 376 |
|
| 377 |
let cleanup = null;
|
| 378 |
onMount(() => {
|
|
|
|
| 380 |
hostEl.__setTheme = setTheme;
|
| 381 |
|
| 382 |
// Jitter & Simulate functions
|
| 383 |
+
function rebuildLegend() {
|
| 384 |
updateDynamicPalette(); // Update colors when adding new data
|
| 385 |
+
legendItems = currentRunList.map((name) => ({
|
| 386 |
+
name,
|
| 387 |
+
color: colorForRun(name),
|
| 388 |
+
}));
|
| 389 |
}
|
| 390 |
+
|
| 391 |
+
function simulateData() {
|
| 392 |
// Generate new random data with weighted probability for fewer runs
|
| 393 |
// Higher probability for 2-3 runs, lower for 4-5-6 runs
|
| 394 |
const rand = Math.random();
|
| 395 |
let wantRuns;
|
| 396 |
+
if (rand < 0.4)
|
| 397 |
+
wantRuns = 2; // 40% chance
|
| 398 |
+
else if (rand < 0.7)
|
| 399 |
+
wantRuns = 3; // 30% chance
|
| 400 |
+
else if (rand < 0.85)
|
| 401 |
+
wantRuns = 4; // 15% chance
|
| 402 |
+
else if (rand < 0.95)
|
| 403 |
+
wantRuns = 5; // 10% chance
|
| 404 |
+
else wantRuns = 6; // 5% chance
|
| 405 |
// Use realistic ML training step counts with cycling scenarios
|
| 406 |
let stepsCount;
|
| 407 |
if (cycleIdx === 0) {
|
| 408 |
+
stepsCount = Random.trainingStepsForScenario("prototyping");
|
| 409 |
} else if (cycleIdx === 1) {
|
| 410 |
+
stepsCount = Random.trainingStepsForScenario("development");
|
| 411 |
} else if (cycleIdx === 2) {
|
| 412 |
+
stepsCount = Random.trainingStepsForScenario("production");
|
| 413 |
} else if (cycleIdx === 3) {
|
| 414 |
+
stepsCount = Random.trainingStepsForScenario("research");
|
| 415 |
} else if (cycleIdx === 4) {
|
| 416 |
+
stepsCount = Random.trainingStepsForScenario("llm");
|
| 417 |
} else if (cycleIdx === 5) {
|
| 418 |
+
stepsCount = Random.trainingStepsForScenario("massive");
|
| 419 |
} else {
|
| 420 |
stepsCount = Random.trainingSteps(); // Full range for variety
|
| 421 |
}
|
| 422 |
cycleIdx = (cycleIdx + 1) % 7; // Cycle through 7 scenarios now
|
| 423 |
+
|
| 424 |
const runsSim = generateRunNames(wantRuns, stepsCount);
|
| 425 |
+
const steps = Array.from({ length: stepsCount }, (_, i) => i + 1);
|
| 426 |
const nextByMetric = new Map();
|
| 427 |
+
const TARGET_METRICS = [
|
| 428 |
+
"epoch",
|
| 429 |
+
"train_accuracy",
|
| 430 |
+
"train_loss",
|
| 431 |
+
"val_accuracy",
|
| 432 |
+
"val_loss",
|
| 433 |
+
];
|
| 434 |
+
const mList =
|
| 435 |
+
metricsToDraw && metricsToDraw.length ? metricsToDraw : TARGET_METRICS;
|
| 436 |
mList.forEach((tgt) => {
|
| 437 |
const map = {};
|
| 438 |
+
runsSim.forEach((r) => {
|
| 439 |
+
map[r] = [];
|
| 440 |
+
});
|
| 441 |
nextByMetric.set(tgt, map);
|
| 442 |
});
|
| 443 |
+
runsSim.forEach((run) => {
|
| 444 |
const curves = genCurves(stepsCount);
|
| 445 |
+
steps.forEach((s, i) => {
|
| 446 |
+
if (mList.includes("epoch"))
|
| 447 |
+
nextByMetric.get("epoch")[run].push({ step: s, value: s });
|
| 448 |
+
if (mList.includes("train_accuracy"))
|
| 449 |
+
nextByMetric
|
| 450 |
+
.get("train_accuracy")
|
| 451 |
+
[run].push({ step: s, value: curves.accTrain[i] });
|
| 452 |
+
if (mList.includes("val_accuracy"))
|
| 453 |
+
nextByMetric
|
| 454 |
+
.get("val_accuracy")
|
| 455 |
+
[run].push({ step: s, value: curves.accVal[i] });
|
| 456 |
+
if (mList.includes("train_loss"))
|
| 457 |
+
nextByMetric
|
| 458 |
+
.get("train_loss")
|
| 459 |
+
[run].push({ step: s, value: curves.lossTrain[i] });
|
| 460 |
+
if (mList.includes("val_loss"))
|
| 461 |
+
nextByMetric
|
| 462 |
+
.get("val_loss")
|
| 463 |
+
[run].push({ step: s, value: curves.lossVal[i] });
|
| 464 |
});
|
| 465 |
});
|
| 466 |
+
nextByMetric.forEach((v, k) => dataByMetric.set(k, v));
|
| 467 |
currentRunList = runsSim.slice();
|
| 468 |
rebuildLegend();
|
| 469 |
updatePreparedData();
|
| 470 |
updateDynamicPalette(); // Update colors when rebuilding
|
| 471 |
+
colorsByRun = Object.fromEntries(
|
| 472 |
+
currentRunList.map((name) => [name, colorForRun(name)]),
|
| 473 |
+
);
|
| 474 |
}
|
| 475 |
// No need for event listeners anymore - we'll use reactive statement
|
| 476 |
|
|
|
|
| 478 |
simulateData();
|
| 479 |
// Svelte Cells will react to preparedData/colorsByRun updates
|
| 480 |
|
| 481 |
+
cleanup = () => {
|
| 482 |
// No cleanup needed for reactive statements
|
| 483 |
};
|
| 484 |
});
|
| 485 |
|
| 486 |
+
onDestroy(() => {
|
| 487 |
+
if (cleanup) cleanup();
|
| 488 |
+
});
|
| 489 |
|
| 490 |
// Expose instance for debugging and external theme control
|
| 491 |
onMount(() => {
|
| 492 |
+
window.trackioInstance = {
|
| 493 |
+
jitterData,
|
| 494 |
+
addLiveDataPoint,
|
| 495 |
+
generateMassiveDataset,
|
| 496 |
+
};
|
| 497 |
if (hostEl) {
|
| 498 |
+
hostEl.__trackioInstance = {
|
| 499 |
+
setTheme,
|
| 500 |
+
setLogScaleX,
|
| 501 |
+
setSmoothing,
|
| 502 |
+
jitterData,
|
| 503 |
+
addLiveDataPoint,
|
| 504 |
+
generateMassiveDataset,
|
| 505 |
+
};
|
| 506 |
}
|
| 507 |
+
|
| 508 |
// Initialize dynamic palette
|
| 509 |
updateDynamicPalette();
|
| 510 |
+
|
| 511 |
// Listen for palette updates from color-palettes.js
|
| 512 |
const handlePaletteUpdate = () => {
|
| 513 |
updateDynamicPalette();
|
| 514 |
// Rebuild legend and colors if needed
|
| 515 |
if (currentRunList.length > 0) {
|
| 516 |
+
legendItems = currentRunList.map((name) => ({
|
| 517 |
+
name,
|
| 518 |
+
color: colorForRun(name),
|
| 519 |
+
}));
|
| 520 |
+
colorsByRun = Object.fromEntries(
|
| 521 |
+
currentRunList.map((name) => [name, colorForRun(name)]),
|
| 522 |
+
);
|
| 523 |
}
|
| 524 |
};
|
| 525 |
+
|
| 526 |
+
document.addEventListener("palettes:updated", handlePaletteUpdate);
|
| 527 |
+
|
| 528 |
// Cleanup listener on destroy
|
| 529 |
return () => {
|
| 530 |
+
document.removeEventListener("palettes:updated", handlePaletteUpdate);
|
| 531 |
};
|
| 532 |
});
|
| 533 |
|
| 534 |
// React to jitter trigger from store
|
| 535 |
$: {
|
| 536 |
+
console.log(
|
| 537 |
+
"Reactive statement triggered, jitterTrigger value:",
|
| 538 |
+
$jitterTrigger,
|
| 539 |
+
);
|
| 540 |
if ($jitterTrigger > 0) {
|
| 541 |
+
console.log(
|
| 542 |
+
"Jitter trigger activated:",
|
| 543 |
+
$jitterTrigger,
|
| 544 |
+
"calling jitterData()",
|
| 545 |
+
);
|
| 546 |
jitterData();
|
| 547 |
}
|
| 548 |
}
|
| 549 |
|
| 550 |
// Legend ghost helpers (hover effects)
|
| 551 |
+
function ghostRun(run) {
|
| 552 |
try {
|
| 553 |
+
hostEl.classList.add("hovering");
|
| 554 |
+
|
| 555 |
// Ghost the chart lines and points
|
| 556 |
+
hostEl.querySelectorAll(".cell").forEach((cell) => {
|
| 557 |
+
cell
|
| 558 |
+
.querySelectorAll("svg .lines path.run-line")
|
| 559 |
+
.forEach((p) =>
|
| 560 |
+
p.classList.toggle("ghost", p.getAttribute("data-run") !== run),
|
| 561 |
+
);
|
| 562 |
+
cell
|
| 563 |
+
.querySelectorAll("svg .lines path.raw-line")
|
| 564 |
+
.forEach((p) =>
|
| 565 |
+
p.classList.toggle("ghost", p.getAttribute("data-run") !== run),
|
| 566 |
+
);
|
| 567 |
+
cell
|
| 568 |
+
.querySelectorAll("svg .points circle.pt")
|
| 569 |
+
.forEach((c) =>
|
| 570 |
+
c.classList.toggle("ghost", c.getAttribute("data-run") !== run),
|
| 571 |
+
);
|
| 572 |
});
|
| 573 |
+
|
| 574 |
// Ghost the legend items
|
| 575 |
+
hostEl.querySelectorAll(".legend-bottom .item").forEach((item) => {
|
| 576 |
+
const itemRun = item.getAttribute("data-run");
|
| 577 |
+
item.classList.toggle("ghost", itemRun !== run);
|
| 578 |
});
|
| 579 |
+
} catch (_) {}
|
| 580 |
}
|
| 581 |
+
function clearGhost() {
|
| 582 |
try {
|
| 583 |
+
hostEl.classList.remove("hovering");
|
| 584 |
+
|
| 585 |
// Clear ghost from chart lines and points
|
| 586 |
+
hostEl.querySelectorAll(".cell").forEach((cell) => {
|
| 587 |
+
cell
|
| 588 |
+
.querySelectorAll("svg .lines path.run-line")
|
| 589 |
+
.forEach((p) => p.classList.remove("ghost"));
|
| 590 |
+
cell
|
| 591 |
+
.querySelectorAll("svg .lines path.raw-line")
|
| 592 |
+
.forEach((p) => p.classList.remove("ghost"));
|
| 593 |
+
cell
|
| 594 |
+
.querySelectorAll("svg .points circle.pt")
|
| 595 |
+
.forEach((c) => c.classList.remove("ghost"));
|
| 596 |
});
|
| 597 |
+
|
| 598 |
// Clear ghost from legend items
|
| 599 |
+
hostEl.querySelectorAll(".legend-bottom .item").forEach((item) => {
|
| 600 |
+
item.classList.remove("ghost");
|
| 601 |
});
|
| 602 |
+
} catch (_) {}
|
| 603 |
}
|
| 604 |
</script>
|
| 605 |
|
| 606 |
<div class="trackio theme--classic" bind:this={hostEl} data-variant={variant}>
|
| 607 |
<div class="trackio__header">
|
| 608 |
+
<Legend
|
| 609 |
+
items={legendItems}
|
| 610 |
+
on:legend-hover={(e) => {
|
| 611 |
+
const run = e?.detail?.name;
|
| 612 |
+
if (!run) return;
|
| 613 |
+
ghostRun(run);
|
| 614 |
+
}}
|
| 615 |
+
on:legend-leave={() => {
|
| 616 |
+
clearGhost();
|
| 617 |
+
}}
|
| 618 |
+
/>
|
| 619 |
</div>
|
| 620 |
<div class="trackio__grid" bind:this={gridEl}>
|
| 621 |
{#each cellsDef as c, i}
|
| 622 |
+
<Cell
|
| 623 |
+
metricKey={c.metric}
|
| 624 |
+
titleText={c.title}
|
| 625 |
+
wide={c.wide}
|
| 626 |
+
{variant}
|
| 627 |
+
{normalizeLoss}
|
| 628 |
+
{logScaleX}
|
| 629 |
+
{smoothing}
|
| 630 |
+
metricData={(preparedData && preparedData[c.metric]) || {}}
|
| 631 |
+
rawMetricData={(preparedRawData && preparedRawData[c.metric]) || {}}
|
| 632 |
+
colorForRun={(name) => colorsByRun[name] || "#999"}
|
| 633 |
{hostEl}
|
| 634 |
currentIndex={i}
|
| 635 |
onOpenModal={openModal}
|
|
|
|
| 638 |
</div>
|
| 639 |
<div class="trackio__footer">
|
| 640 |
<small>
|
| 641 |
+
Built with <a
|
| 642 |
+
href="https://github.com/huggingface/trackio"
|
| 643 |
+
target="_blank"
|
| 644 |
+
rel="noopener noreferrer">TrackIO</a
|
| 645 |
+
>
|
| 646 |
<span class="separator">•</span>
|
| 647 |
+
<a
|
| 648 |
+
href="https://huggingface.co/docs/hub/spaces-sdks-docker"
|
| 649 |
+
target="_blank"
|
| 650 |
+
rel="noopener noreferrer">Use via API</a
|
| 651 |
+
>
|
| 652 |
</small>
|
| 653 |
</div>
|
| 654 |
</div>
|
|
|
|
| 656 |
<!-- Centralized Fullscreen Modal -->
|
| 657 |
<FullscreenModal
|
| 658 |
visible={isModalOpen}
|
| 659 |
+
title={allChartsData[currentFullscreenIndex]?.titleText || ""}
|
| 660 |
metricData={allChartsData[currentFullscreenIndex]?.metricData || {}}
|
| 661 |
rawMetricData={allChartsData[currentFullscreenIndex]?.rawMetricData || {}}
|
| 662 |
colorForRun={modalColorForRun}
|
|
|
|
| 664 |
{logScaleX}
|
| 665 |
{smoothing}
|
| 666 |
{normalizeLoss}
|
| 667 |
+
metricKey={allChartsData[currentFullscreenIndex]?.metricKey || ""}
|
| 668 |
+
titleText={allChartsData[currentFullscreenIndex]?.titleText || ""}
|
| 669 |
currentIndex={currentFullscreenIndex}
|
| 670 |
totalCharts={cellsDef.length}
|
| 671 |
onNavigate={handleNavigate}
|
|
|
|
| 678 |
========================= */
|
| 679 |
|
| 680 |
/* Font imports for themes - ensure Roboto Mono is loaded for Oblivion theme */
|
| 681 |
+
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap");
|
| 682 |
+
|
| 683 |
/* Fallback font-face declaration */
|
| 684 |
@font-face {
|
| 685 |
+
font-family: "Roboto Mono Fallback";
|
| 686 |
+
src: url("https://fonts.gstatic.com/s/robotomono/v23/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW4AJi8SJQt.woff2")
|
| 687 |
+
format("woff2");
|
| 688 |
font-weight: 400;
|
| 689 |
font-style: normal;
|
| 690 |
font-display: swap;
|
|
|
|
| 695 |
position: relative;
|
| 696 |
--z-tooltip: 50;
|
| 697 |
--z-overlay: 99999999;
|
| 698 |
+
|
| 699 |
/* Typography */
|
| 700 |
+
--trackio-font-family: var(
|
| 701 |
+
--font-mono,
|
| 702 |
+
ui-monospace,
|
| 703 |
+
SFMono-Regular,
|
| 704 |
+
Menlo,
|
| 705 |
+
monospace
|
| 706 |
+
);
|
| 707 |
--trackio-font-weight-normal: 400;
|
| 708 |
--trackio-font-weight-medium: 600;
|
| 709 |
--trackio-font-weight-bold: 700;
|
| 710 |
+
|
| 711 |
/* Apply font-family to root element */
|
| 712 |
font-family: var(--trackio-font-family);
|
| 713 |
+
|
| 714 |
/* Base color system for Classic theme */
|
| 715 |
--trackio-base: #323232;
|
| 716 |
--trackio-primary: var(--trackio-base);
|
| 717 |
--trackio-dim: color-mix(in srgb, var(--trackio-base) 28%, transparent);
|
| 718 |
--trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
|
| 719 |
--trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
|
| 720 |
+
|
| 721 |
/* Chart rendering */
|
| 722 |
+
--trackio-chart-grid-type: "lines"; /* 'lines' | 'dots' */
|
| 723 |
--trackio-chart-axis-stroke: var(--trackio-dim);
|
| 724 |
--trackio-chart-axis-text: var(--trackio-text);
|
| 725 |
--trackio-chart-grid-stroke: var(--trackio-subtle);
|
| 726 |
--trackio-chart-grid-opacity: 1;
|
| 727 |
}
|
| 728 |
+
|
| 729 |
/* Dark mode overrides for Classic theme */
|
| 730 |
:global([data-theme="dark"]) .trackio.theme--classic {
|
| 731 |
--trackio-base: #ffffff;
|
|
|
|
| 733 |
--trackio-dim: color-mix(in srgb, var(--trackio-base) 25%, transparent);
|
| 734 |
--trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
|
| 735 |
--trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
|
| 736 |
+
|
| 737 |
/* Cell background for dark mode */
|
| 738 |
--trackio-cell-background: rgba(255, 255, 255, 0.03);
|
| 739 |
}
|
| 740 |
+
|
| 741 |
.trackio.theme--classic {
|
| 742 |
/* Cell styling */
|
| 743 |
--trackio-cell-background: rgba(0, 0, 0, 0.02);
|
| 744 |
--trackio-cell-border: var(--border-color, rgba(0, 0, 0, 0.1));
|
| 745 |
--trackio-cell-corner-inset: 0px;
|
| 746 |
--trackio-cell-gap: 12px;
|
| 747 |
+
|
| 748 |
/* Typography */
|
| 749 |
--trackio-text-primary: var(--text-color, rgba(0, 0, 0, 0.9));
|
| 750 |
--trackio-text-secondary: var(--muted-color, rgba(0, 0, 0, 0.6));
|
| 751 |
+
--trackio-text-accent: var(--primary-color);
|
| 752 |
+
|
| 753 |
/* Tooltip */
|
| 754 |
--trackio-tooltip-background: var(--surface-bg, white);
|
| 755 |
--trackio-tooltip-border: var(--border-color, rgba(0, 0, 0, 0.1));
|
| 756 |
--trackio-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
| 757 |
+
|
| 758 |
/* Legend */
|
| 759 |
--trackio-legend-text: var(--text-color, rgba(0, 0, 0, 0.9));
|
| 760 |
--trackio-legend-swatch-border: var(--border-color, rgba(0, 0, 0, 0.1));
|
|
|
|
| 763 |
/* Dark mode adjustments */
|
| 764 |
:global([data-theme="dark"]) .trackio {
|
| 765 |
--trackio-chart-axis-stroke: rgba(255, 255, 255, 0.18);
|
| 766 |
+
--trackio-chart-axis-text: rgba(255, 255, 255, 0.6);
|
| 767 |
--trackio-chart-grid-stroke: rgba(255, 255, 255, 0.08);
|
| 768 |
}
|
| 769 |
|
| 770 |
/* =========================
|
| 771 |
THEME: CLASSIC (Default)
|
| 772 |
========================= */
|
| 773 |
+
|
| 774 |
.trackio.theme--classic {
|
| 775 |
/* Keep default values - no overrides needed */
|
| 776 |
}
|
|
|
|
| 778 |
/* =========================
|
| 779 |
THEME: OBLIVION
|
| 780 |
========================= */
|
| 781 |
+
|
| 782 |
.trackio.theme--oblivion {
|
| 783 |
/* Core oblivion color system - Light mode: darker colors for visibility */
|
| 784 |
--trackio-oblivion-base: #2a2a2a;
|
| 785 |
--trackio-oblivion-primary: var(--trackio-oblivion-base);
|
| 786 |
+
--trackio-oblivion-dim: color-mix(
|
| 787 |
+
in srgb,
|
| 788 |
+
var(--trackio-oblivion-base) 30%,
|
| 789 |
+
transparent
|
| 790 |
+
);
|
| 791 |
+
--trackio-oblivion-subtle: color-mix(
|
| 792 |
+
in srgb,
|
| 793 |
+
var(--trackio-oblivion-base) 8%,
|
| 794 |
+
transparent
|
| 795 |
+
);
|
| 796 |
+
--trackio-oblivion-ghost: color-mix(
|
| 797 |
+
in srgb,
|
| 798 |
+
var(--trackio-oblivion-base) 4%,
|
| 799 |
+
transparent
|
| 800 |
+
);
|
| 801 |
+
|
| 802 |
/* Chart rendering overrides */
|
| 803 |
+
--trackio-chart-grid-type: "dots";
|
| 804 |
--trackio-chart-axis-stroke: var(--trackio-oblivion-dim);
|
| 805 |
--trackio-chart-axis-text: var(--trackio-oblivion-primary);
|
| 806 |
--trackio-chart-grid-stroke: var(--trackio-oblivion-dim);
|
| 807 |
--trackio-chart-grid-opacity: 0.6;
|
| 808 |
}
|
| 809 |
+
|
| 810 |
/* Dark mode overrides for Oblivion theme */
|
| 811 |
:global([data-theme="dark"]) .trackio.theme--oblivion {
|
| 812 |
--trackio-oblivion-base: #ffffff;
|
| 813 |
--trackio-oblivion-primary: var(--trackio-oblivion-base);
|
| 814 |
+
--trackio-oblivion-dim: color-mix(
|
| 815 |
+
in srgb,
|
| 816 |
+
var(--trackio-oblivion-base) 25%,
|
| 817 |
+
transparent
|
| 818 |
+
);
|
| 819 |
+
--trackio-oblivion-subtle: color-mix(
|
| 820 |
+
in srgb,
|
| 821 |
+
var(--trackio-oblivion-base) 8%,
|
| 822 |
+
transparent
|
| 823 |
+
);
|
| 824 |
+
--trackio-oblivion-ghost: color-mix(
|
| 825 |
+
in srgb,
|
| 826 |
+
var(--trackio-oblivion-base) 4%,
|
| 827 |
+
transparent
|
| 828 |
+
);
|
| 829 |
}
|
| 830 |
+
|
| 831 |
.trackio.theme--oblivion {
|
| 832 |
/* Cell styling overrides */
|
| 833 |
--trackio-cell-background: var(--trackio-oblivion-subtle);
|
| 834 |
--trackio-cell-border: var(--trackio-oblivion-dim);
|
| 835 |
--trackio-cell-corner-inset: 6px;
|
| 836 |
--trackio-cell-gap: 0px;
|
| 837 |
+
|
| 838 |
/* HUD-specific variables */
|
| 839 |
--trackio-oblivion-hud-gap: 10px;
|
| 840 |
--trackio-oblivion-hud-corner-size: 8px;
|
| 841 |
+
--trackio-oblivion-hud-bg-gradient: radial-gradient(
|
| 842 |
+
1200px 200px at 20% -10%,
|
| 843 |
+
var(--trackio-oblivion-ghost),
|
| 844 |
+
transparent 80%
|
| 845 |
+
),
|
| 846 |
+
radial-gradient(
|
| 847 |
+
900px 200px at 80% 110%,
|
| 848 |
+
var(--trackio-oblivion-ghost),
|
| 849 |
+
transparent 80%
|
| 850 |
+
);
|
| 851 |
+
|
| 852 |
/* Typography overrides */
|
| 853 |
--trackio-text-primary: var(--trackio-oblivion-primary);
|
| 854 |
--trackio-text-secondary: var(--trackio-oblivion-dim);
|
| 855 |
--trackio-text-accent: var(--trackio-oblivion-primary);
|
| 856 |
+
|
| 857 |
/* Tooltip overrides */
|
| 858 |
--trackio-tooltip-background: var(--trackio-oblivion-subtle);
|
| 859 |
--trackio-tooltip-border: var(--trackio-oblivion-dim);
|
| 860 |
+
--trackio-tooltip-shadow: 0 8px 32px
|
| 861 |
+
color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent),
|
| 862 |
0 2px 8px color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent);
|
| 863 |
+
|
| 864 |
/* Legend overrides */
|
| 865 |
--trackio-legend-text: var(--trackio-oblivion-primary);
|
| 866 |
--trackio-legend-swatch-border: var(--trackio-oblivion-dim);
|
| 867 |
+
|
| 868 |
/* Font styling overrides */
|
| 869 |
+
--trackio-font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
|
| 870 |
+
SFMono-Regular, Menlo, monospace;
|
| 871 |
font-family: var(--trackio-font-family) !important;
|
| 872 |
color: var(--trackio-text-primary);
|
| 873 |
}
|
|
|
|
| 875 |
/* Force Roboto Mono application in Oblivion theme */
|
| 876 |
.trackio.theme--oblivion,
|
| 877 |
.trackio.theme--oblivion * {
|
| 878 |
+
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
|
| 879 |
+
SFMono-Regular, Menlo, monospace !important;
|
| 880 |
}
|
| 881 |
+
|
| 882 |
/* Specific overrides for different elements in Oblivion */
|
| 883 |
.trackio.theme--oblivion .cell-title,
|
| 884 |
.trackio.theme--oblivion .legend-bottom,
|
| 885 |
.trackio.theme--oblivion .legend-title,
|
| 886 |
.trackio.theme--oblivion .item {
|
| 887 |
+
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
|
| 888 |
+
SFMono-Regular, Menlo, monospace !important;
|
| 889 |
}
|
| 890 |
|
| 891 |
/* Dark mode adjustments for Oblivion */
|
| 892 |
:global([data-theme="dark"]) .trackio.theme--oblivion {
|
| 893 |
--trackio-oblivion-base: #ffffff;
|
| 894 |
+
--trackio-oblivion-hud-bg-gradient: radial-gradient(
|
| 895 |
+
1400px 260px at 20% -10%,
|
| 896 |
+
color-mix(in srgb, var(--trackio-oblivion-base) 6.5%, transparent),
|
| 897 |
+
transparent 80%
|
| 898 |
+
),
|
| 899 |
+
radial-gradient(
|
| 900 |
+
1100px 240px at 80% 110%,
|
| 901 |
+
color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent),
|
| 902 |
+
transparent 80%
|
| 903 |
+
),
|
| 904 |
+
linear-gradient(
|
| 905 |
+
180deg,
|
| 906 |
+
color-mix(in srgb, var(--trackio-oblivion-base) 3.5%, transparent),
|
| 907 |
+
transparent 45%
|
| 908 |
+
);
|
| 909 |
+
|
| 910 |
+
--trackio-tooltip-shadow: 0 8px 32px
|
| 911 |
+
color-mix(in srgb, var(--trackio-oblivion-base) 5%, transparent),
|
| 912 |
0 2px 8px color-mix(in srgb, black 10%, transparent);
|
| 913 |
+
|
| 914 |
background: #0f1115;
|
| 915 |
}
|
| 916 |
|
|
|
|
| 923 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 924 |
gap: var(--trackio-cell-gap);
|
| 925 |
}
|
| 926 |
+
|
| 927 |
@media (max-width: 980px) {
|
| 928 |
+
.trackio__grid {
|
| 929 |
+
grid-template-columns: 1fr;
|
| 930 |
+
}
|
| 931 |
}
|
| 932 |
|
| 933 |
.trackio__header {
|
|
|
|
| 945 |
.trackio .axes line {
|
| 946 |
stroke: var(--trackio-chart-axis-stroke);
|
| 947 |
}
|
| 948 |
+
|
| 949 |
.trackio .axes text {
|
| 950 |
fill: var(--trackio-chart-axis-text);
|
| 951 |
font-family: var(--trackio-font-family);
|
| 952 |
}
|
| 953 |
+
|
| 954 |
/* Force font-family for SVG text in Oblivion */
|
| 955 |
.trackio.theme--oblivion .axes text {
|
| 956 |
+
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
|
| 957 |
+
SFMono-Regular, Menlo, monospace !important;
|
| 958 |
}
|
| 959 |
+
|
| 960 |
.trackio .grid line {
|
| 961 |
stroke: var(--trackio-chart-grid-stroke);
|
| 962 |
opacity: var(--trackio-chart-grid-opacity);
|
| 963 |
}
|
| 964 |
|
| 965 |
/* Grid type switching */
|
| 966 |
+
.trackio .grid-dots {
|
| 967 |
+
display: none;
|
| 968 |
+
}
|
| 969 |
+
.trackio.theme--oblivion .grid {
|
| 970 |
+
display: none;
|
| 971 |
+
}
|
| 972 |
+
.trackio.theme--oblivion .grid-dots {
|
| 973 |
+
display: block;
|
| 974 |
+
}
|
| 975 |
.trackio.theme--oblivion .cell-bg,
|
| 976 |
+
.trackio.theme--oblivion .cell-corners {
|
| 977 |
+
display: block;
|
| 978 |
+
}
|
| 979 |
|
| 980 |
/* =========================
|
| 981 |
FOOTER
|
|
|
|
| 1019 |
}
|
| 1020 |
|
| 1021 |
.trackio.theme--oblivion .trackio__footer small {
|
| 1022 |
+
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
|
| 1023 |
+
SFMono-Regular, Menlo, monospace !important;
|
| 1024 |
}
|
| 1025 |
</style>
|
|
|
|
|
|
app/src/components/trackio/TrackioWrapper.astro
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
// TrackioWrapper.astro
|
| 3 |
+
import Trackio from "./Trackio.svelte";
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
<!-- Ensure Roboto Mono is loaded for Oblivion theme -->
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
+
|
| 14 |
+
<div class="trackio-wrapper">
|
| 15 |
+
<div class="trackio-controls">
|
| 16 |
+
<div class="controls-left">
|
| 17 |
+
<div class="theme-selector">
|
| 18 |
+
<label for="theme-select">Theme</label>
|
| 19 |
+
<select id="theme-select" class="theme-select">
|
| 20 |
+
<option value="classic">Classic</option>
|
| 21 |
+
<option value="oblivion">Oblivion</option>
|
| 22 |
+
</select>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="scale-controls">
|
| 25 |
+
<label>
|
| 26 |
+
<input type="checkbox" id="log-scale-x" checked />
|
| 27 |
+
Log Scale X
|
| 28 |
+
</label>
|
| 29 |
+
<label>
|
| 30 |
+
<input type="checkbox" id="smooth-data" checked />
|
| 31 |
+
Smooth
|
| 32 |
+
</label>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
<div class="controls-right">
|
| 36 |
+
<button class="button button--ghost" type="button" id="randomize-btn">
|
| 37 |
+
Randomize Data
|
| 38 |
+
</button>
|
| 39 |
+
<button
|
| 40 |
+
class="button button--primary"
|
| 41 |
+
type="button"
|
| 42 |
+
id="start-simulation-btn"
|
| 43 |
+
>
|
| 44 |
+
Live Run
|
| 45 |
+
</button>
|
| 46 |
+
<button
|
| 47 |
+
class="button button--danger"
|
| 48 |
+
type="button"
|
| 49 |
+
id="stop-simulation-btn"
|
| 50 |
+
style="display: none;"
|
| 51 |
+
>
|
| 52 |
+
Stop
|
| 53 |
+
</button>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div class="trackio-container">
|
| 58 |
+
<Trackio client:load variant="classic" logScaleX={true} smoothing={true} />
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<script>
|
| 63 |
+
// @ts-nocheck
|
| 64 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
| 65 |
+
const themeSelect = document.getElementById("theme-select");
|
| 66 |
+
const randomizeBtn = document.getElementById("randomize-btn");
|
| 67 |
+
const startSimulationBtn = document.getElementById("start-simulation-btn");
|
| 68 |
+
const stopSimulationBtn = document.getElementById("stop-simulation-btn");
|
| 69 |
+
const logScaleXCheckbox = document.getElementById("log-scale-x");
|
| 70 |
+
const smoothDataCheckbox = document.getElementById("smooth-data");
|
| 71 |
+
const trackioContainer = document.querySelector(".trackio-container");
|
| 72 |
+
|
| 73 |
+
if (
|
| 74 |
+
!themeSelect ||
|
| 75 |
+
!randomizeBtn ||
|
| 76 |
+
!startSimulationBtn ||
|
| 77 |
+
!stopSimulationBtn ||
|
| 78 |
+
!logScaleXCheckbox ||
|
| 79 |
+
!smoothDataCheckbox ||
|
| 80 |
+
!trackioContainer
|
| 81 |
+
)
|
| 82 |
+
return;
|
| 83 |
+
|
| 84 |
+
// Variables for simulation
|
| 85 |
+
let simulationInterval = null;
|
| 86 |
+
let currentSimulationRun = null;
|
| 87 |
+
let currentStep = 0;
|
| 88 |
+
|
| 89 |
+
// Import the store function
|
| 90 |
+
const { triggerJitter } = await import("./core/store.js");
|
| 91 |
+
|
| 92 |
+
// Theme change handler
|
| 93 |
+
themeSelect.addEventListener("change", (e) => {
|
| 94 |
+
const target = e.target;
|
| 95 |
+
if (!target || !("value" in target)) return;
|
| 96 |
+
|
| 97 |
+
const newVariant = target.value;
|
| 98 |
+
console.log(`Theme changed to: ${newVariant}`); // Debug log
|
| 99 |
+
|
| 100 |
+
// Find the trackio element and call setTheme on the Svelte instance
|
| 101 |
+
const trackioEl = debugTrackioState();
|
| 102 |
+
if (trackioEl && trackioEl.__trackioInstance) {
|
| 103 |
+
console.log("✅ Calling setTheme on Trackio instance");
|
| 104 |
+
trackioEl.__trackioInstance.setTheme(newVariant);
|
| 105 |
+
} else {
|
| 106 |
+
console.warn("❌ No Trackio instance found for theme change");
|
| 107 |
+
}
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// Log scale X change handler
|
| 111 |
+
logScaleXCheckbox.addEventListener("change", (e) => {
|
| 112 |
+
const target = e.target;
|
| 113 |
+
if (!target || !("checked" in target)) return;
|
| 114 |
+
|
| 115 |
+
const isLogScale = target.checked;
|
| 116 |
+
console.log(`Log scale X changed to: ${isLogScale}`); // Debug log
|
| 117 |
+
|
| 118 |
+
// Find the trackio element and call setLogScaleX on the Svelte instance
|
| 119 |
+
const trackioEl = debugTrackioState();
|
| 120 |
+
if (trackioEl && trackioEl.__trackioInstance) {
|
| 121 |
+
console.log("✅ Calling setLogScaleX on Trackio instance");
|
| 122 |
+
trackioEl.__trackioInstance.setLogScaleX(isLogScale);
|
| 123 |
+
} else {
|
| 124 |
+
console.warn("❌ Trackio instance not found for log scale change");
|
| 125 |
+
}
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
// Smooth data change handler
|
| 129 |
+
smoothDataCheckbox.addEventListener("change", (e) => {
|
| 130 |
+
const target = e.target;
|
| 131 |
+
if (!target || !("checked" in target)) return;
|
| 132 |
+
|
| 133 |
+
const isSmooth = target.checked;
|
| 134 |
+
console.log(`Smooth data changed to: ${isSmooth}`); // Debug log
|
| 135 |
+
|
| 136 |
+
// Find the trackio element and call setSmoothing on the Svelte instance
|
| 137 |
+
const trackioEl = debugTrackioState();
|
| 138 |
+
if (trackioEl && trackioEl.__trackioInstance) {
|
| 139 |
+
console.log("✅ Calling setSmoothing on Trackio instance");
|
| 140 |
+
trackioEl.__trackioInstance.setSmoothing(isSmooth);
|
| 141 |
+
} else {
|
| 142 |
+
console.warn("❌ Trackio instance not found for smooth change");
|
| 143 |
+
}
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
// Debug function to check trackio state
|
| 147 |
+
function debugTrackioState() {
|
| 148 |
+
const trackioEl = trackioContainer.querySelector(".trackio");
|
| 149 |
+
console.log("�� Debug Trackio state:", {
|
| 150 |
+
container: !!trackioContainer,
|
| 151 |
+
trackioEl: !!trackioEl,
|
| 152 |
+
hasInstance: !!(trackioEl && trackioEl.__trackioInstance),
|
| 153 |
+
availableMethods:
|
| 154 |
+
trackioEl && trackioEl.__trackioInstance
|
| 155 |
+
? Object.keys(trackioEl.__trackioInstance)
|
| 156 |
+
: "none",
|
| 157 |
+
windowInstance: !!window.trackioInstance,
|
| 158 |
+
});
|
| 159 |
+
return trackioEl;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// Initialize with default checked states - increased delay and retry logic
|
| 163 |
+
function initializeTrackio(attempt = 1) {
|
| 164 |
+
console.log(`🚀 Initializing Trackio (attempt ${attempt})`);
|
| 165 |
+
|
| 166 |
+
const trackioEl = debugTrackioState();
|
| 167 |
+
|
| 168 |
+
if (trackioEl && trackioEl.__trackioInstance) {
|
| 169 |
+
console.log("✅ Trackio instance found, applying initial settings");
|
| 170 |
+
|
| 171 |
+
if (logScaleXCheckbox.checked) {
|
| 172 |
+
console.log("Initializing with log scale X enabled");
|
| 173 |
+
trackioEl.__trackioInstance.setLogScaleX(true);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if (smoothDataCheckbox.checked) {
|
| 177 |
+
console.log("Initializing with smoothing enabled");
|
| 178 |
+
trackioEl.__trackioInstance.setSmoothing(true);
|
| 179 |
+
}
|
| 180 |
+
} else {
|
| 181 |
+
console.log("❌ Trackio instance not ready yet");
|
| 182 |
+
if (attempt < 10) {
|
| 183 |
+
setTimeout(() => initializeTrackio(attempt + 1), 200 * attempt);
|
| 184 |
+
} else {
|
| 185 |
+
console.error("Failed to initialize Trackio after 10 attempts");
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Start initialization
|
| 191 |
+
setTimeout(() => initializeTrackio(), 100);
|
| 192 |
+
|
| 193 |
+
// Function to generate a new simulated metric value
|
| 194 |
+
function generateSimulatedValue(step, metric) {
|
| 195 |
+
const baseProgress = Math.min(1, step / 100); // Normalise sur 100 steps
|
| 196 |
+
|
| 197 |
+
if (metric === "loss") {
|
| 198 |
+
// Loss that decreases with noise
|
| 199 |
+
const baseLoss = 2.0 * Math.exp(-0.05 * step);
|
| 200 |
+
const noise = (Math.random() - 0.5) * 0.2;
|
| 201 |
+
return Math.max(0.01, baseLoss + noise);
|
| 202 |
+
} else if (metric === "accuracy") {
|
| 203 |
+
// Accuracy that increases with noise
|
| 204 |
+
const baseAcc = 0.1 + 0.8 * (1 - Math.exp(-0.04 * step));
|
| 205 |
+
const noise = (Math.random() - 0.5) * 0.05;
|
| 206 |
+
return Math.max(0, Math.min(1, baseAcc + noise));
|
| 207 |
+
}
|
| 208 |
+
return Math.random();
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Handler to start simulation
|
| 212 |
+
function startSimulation() {
|
| 213 |
+
if (simulationInterval) {
|
| 214 |
+
clearInterval(simulationInterval);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Générer un nouveau nom de run
|
| 218 |
+
const adjectives = [
|
| 219 |
+
"live",
|
| 220 |
+
"real-time",
|
| 221 |
+
"streaming",
|
| 222 |
+
"dynamic",
|
| 223 |
+
"active",
|
| 224 |
+
"running",
|
| 225 |
+
];
|
| 226 |
+
const nouns = [
|
| 227 |
+
"experiment",
|
| 228 |
+
"trial",
|
| 229 |
+
"session",
|
| 230 |
+
"training",
|
| 231 |
+
"run",
|
| 232 |
+
"test",
|
| 233 |
+
];
|
| 234 |
+
const randomAdj =
|
| 235 |
+
adjectives[Math.floor(Math.random() * adjectives.length)];
|
| 236 |
+
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
|
| 237 |
+
currentSimulationRun = `${randomAdj}-${randomNoun}-${Date.now().toString().slice(-4)}`;
|
| 238 |
+
currentStep = 1; // Commencer à step 1
|
| 239 |
+
|
| 240 |
+
console.log(`Starting simulation for run: ${currentSimulationRun}`);
|
| 241 |
+
|
| 242 |
+
// Interface UI
|
| 243 |
+
startSimulationBtn.style.display = "none";
|
| 244 |
+
stopSimulationBtn.style.display = "inline-flex";
|
| 245 |
+
startSimulationBtn.disabled = true;
|
| 246 |
+
|
| 247 |
+
// Ajouter le premier point
|
| 248 |
+
addSimulationStep();
|
| 249 |
+
|
| 250 |
+
// Continuer chaque seconde
|
| 251 |
+
simulationInterval = setInterval(() => {
|
| 252 |
+
currentStep++;
|
| 253 |
+
addSimulationStep();
|
| 254 |
+
|
| 255 |
+
// Stop after 200 steps to avoid infinity
|
| 256 |
+
if (currentStep > 200) {
|
| 257 |
+
stopSimulation();
|
| 258 |
+
}
|
| 259 |
+
}, 1000); // Chaque seconde
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// Function to add a new data point
|
| 263 |
+
function addSimulationStep() {
|
| 264 |
+
const trackioEl = trackioContainer.querySelector(".trackio");
|
| 265 |
+
if (trackioEl && trackioEl.__trackioInstance) {
|
| 266 |
+
const newDataPoint = {
|
| 267 |
+
step: currentStep,
|
| 268 |
+
loss: generateSimulatedValue(currentStep, "loss"),
|
| 269 |
+
accuracy: generateSimulatedValue(currentStep, "accuracy"),
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
console.log(
|
| 273 |
+
`Adding simulation step ${currentStep} for run ${currentSimulationRun}:`,
|
| 274 |
+
newDataPoint,
|
| 275 |
+
);
|
| 276 |
+
|
| 277 |
+
// Ajouter le point via l'instance Trackio
|
| 278 |
+
if (
|
| 279 |
+
typeof trackioEl.__trackioInstance.addLiveDataPoint === "function"
|
| 280 |
+
) {
|
| 281 |
+
trackioEl.__trackioInstance.addLiveDataPoint(
|
| 282 |
+
currentSimulationRun,
|
| 283 |
+
newDataPoint,
|
| 284 |
+
);
|
| 285 |
+
} else {
|
| 286 |
+
console.warn("addLiveDataPoint method not found on Trackio instance");
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// Handler to stop simulation
|
| 292 |
+
function stopSimulation() {
|
| 293 |
+
if (simulationInterval) {
|
| 294 |
+
clearInterval(simulationInterval);
|
| 295 |
+
simulationInterval = null;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
console.log(`Stopping simulation for run: ${currentSimulationRun}`);
|
| 299 |
+
|
| 300 |
+
// Interface UI
|
| 301 |
+
startSimulationBtn.style.display = "inline-flex";
|
| 302 |
+
stopSimulationBtn.style.display = "none";
|
| 303 |
+
startSimulationBtn.disabled = false;
|
| 304 |
+
|
| 305 |
+
currentSimulationRun = null;
|
| 306 |
+
currentStep = 0;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Event listeners for simulation buttons
|
| 310 |
+
startSimulationBtn.addEventListener("click", startSimulation);
|
| 311 |
+
stopSimulationBtn.addEventListener("click", stopSimulation);
|
| 312 |
+
|
| 313 |
+
// Arrêter la simulation si l'utilisateur quitte la page
|
| 314 |
+
window.addEventListener("beforeunload", stopSimulation);
|
| 315 |
+
|
| 316 |
+
// Randomize data handler - now uses the store
|
| 317 |
+
randomizeBtn.addEventListener("click", () => {
|
| 318 |
+
console.log("Randomize button clicked - triggering jitter via store"); // Debug log
|
| 319 |
+
|
| 320 |
+
// Arrêter la simulation en cours si elle tourne
|
| 321 |
+
if (simulationInterval) {
|
| 322 |
+
stopSimulation();
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// Add vibration animation
|
| 326 |
+
randomizeBtn.classList.add("vibrating");
|
| 327 |
+
setTimeout(() => {
|
| 328 |
+
randomizeBtn.classList.remove("vibrating");
|
| 329 |
+
}, 600);
|
| 330 |
+
|
| 331 |
+
// Test direct window approach as well
|
| 332 |
+
if (
|
| 333 |
+
window.trackioInstance &&
|
| 334 |
+
typeof window.trackioInstance.jitterData === "function"
|
| 335 |
+
) {
|
| 336 |
+
console.log(
|
| 337 |
+
"Found window.trackioInstance, calling jitterData directly",
|
| 338 |
+
); // Debug log
|
| 339 |
+
window.trackioInstance.jitterData();
|
| 340 |
+
} else {
|
| 341 |
+
console.log("No window.trackioInstance found, using store trigger"); // Debug log
|
| 342 |
+
triggerJitter();
|
| 343 |
+
}
|
| 344 |
+
});
|
| 345 |
+
});
|
| 346 |
+
</script>
|
| 347 |
+
|
| 348 |
+
<style>
|
| 349 |
+
.trackio-wrapper {
|
| 350 |
+
width: 100%;
|
| 351 |
+
margin: 0px 0 20px 0;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.trackio-controls {
|
| 355 |
+
display: flex;
|
| 356 |
+
justify-content: space-between;
|
| 357 |
+
align-items: center;
|
| 358 |
+
margin-bottom: 16px;
|
| 359 |
+
padding: 12px 0px;
|
| 360 |
+
/* border-bottom: 1px solid var(--border-color); */
|
| 361 |
+
gap: 16px;
|
| 362 |
+
flex-wrap: nowrap;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
.controls-left {
|
| 366 |
+
display: flex;
|
| 367 |
+
align-items: center;
|
| 368 |
+
gap: 24px;
|
| 369 |
+
flex-wrap: wrap;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.controls-right {
|
| 373 |
+
display: flex;
|
| 374 |
+
align-items: center;
|
| 375 |
+
gap: 12px;
|
| 376 |
+
flex-wrap: wrap;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.btn-randomize {
|
| 380 |
+
display: inline-flex;
|
| 381 |
+
align-items: center;
|
| 382 |
+
gap: 6px;
|
| 383 |
+
padding: 8px 16px;
|
| 384 |
+
background: var(--accent-color, #007acc);
|
| 385 |
+
color: white;
|
| 386 |
+
border: none;
|
| 387 |
+
border-radius: 6px;
|
| 388 |
+
font-size: 14px;
|
| 389 |
+
font-weight: 500;
|
| 390 |
+
cursor: pointer;
|
| 391 |
+
transition: all 0.15s ease;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.btn-randomize:hover {
|
| 395 |
+
background: var(--accent-hover, #005a9e);
|
| 396 |
+
transform: translateY(-1px);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.btn-randomize:active {
|
| 400 |
+
transform: translateY(0);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.theme-selector {
|
| 404 |
+
display: flex;
|
| 405 |
+
align-items: center;
|
| 406 |
+
gap: 8px;
|
| 407 |
+
font-size: 14px;
|
| 408 |
+
flex-shrink: 0;
|
| 409 |
+
white-space: nowrap;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.theme-selector label {
|
| 413 |
+
font-weight: 500;
|
| 414 |
+
color: var(--text-color);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.theme-select {
|
| 418 |
+
padding: 6px 12px;
|
| 419 |
+
border: 1px solid var(--border-color);
|
| 420 |
+
border-radius: 4px;
|
| 421 |
+
background: var(--input-bg, var(--surface-bg));
|
| 422 |
+
color: var(--text-color);
|
| 423 |
+
font-size: 14px;
|
| 424 |
+
cursor: pointer;
|
| 425 |
+
transition: border-color 0.15s ease;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.theme-select:focus {
|
| 429 |
+
outline: none;
|
| 430 |
+
border-color: var(--accent-color, #007acc);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.scale-controls {
|
| 434 |
+
display: flex;
|
| 435 |
+
align-items: center;
|
| 436 |
+
gap: 16px;
|
| 437 |
+
flex-shrink: 0;
|
| 438 |
+
white-space: nowrap;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
/* Vibration animation for button */
|
| 442 |
+
@keyframes vibrate {
|
| 443 |
+
0% {
|
| 444 |
+
transform: translateX(0);
|
| 445 |
+
}
|
| 446 |
+
10% {
|
| 447 |
+
transform: translateX(-2px) rotate(-1deg);
|
| 448 |
+
}
|
| 449 |
+
20% {
|
| 450 |
+
transform: translateX(2px) rotate(1deg);
|
| 451 |
+
}
|
| 452 |
+
30% {
|
| 453 |
+
transform: translateX(-2px) rotate(-1deg);
|
| 454 |
+
}
|
| 455 |
+
40% {
|
| 456 |
+
transform: translateX(2px) rotate(1deg);
|
| 457 |
+
}
|
| 458 |
+
50% {
|
| 459 |
+
transform: translateX(-1px) rotate(-0.5deg);
|
| 460 |
+
}
|
| 461 |
+
60% {
|
| 462 |
+
transform: translateX(1px) rotate(0.5deg);
|
| 463 |
+
}
|
| 464 |
+
70% {
|
| 465 |
+
transform: translateX(-1px) rotate(-0.5deg);
|
| 466 |
+
}
|
| 467 |
+
80% {
|
| 468 |
+
transform: translateX(1px) rotate(0.5deg);
|
| 469 |
+
}
|
| 470 |
+
90% {
|
| 471 |
+
transform: translateX(-0.5px) rotate(-0.25deg);
|
| 472 |
+
}
|
| 473 |
+
100% {
|
| 474 |
+
transform: translateX(0) rotate(0);
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.button.vibrating {
|
| 479 |
+
animation: vibrate 0.6s ease-in-out;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.trackio-container {
|
| 483 |
+
width: 100%;
|
| 484 |
+
margin-top: 10px;
|
| 485 |
+
border: 1px solid var(--border-color);
|
| 486 |
+
padding: 24px 12px;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
@media (max-width: 768px) {
|
| 490 |
+
.trackio-controls {
|
| 491 |
+
flex-direction: column;
|
| 492 |
+
align-items: stretch;
|
| 493 |
+
gap: 12px;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.controls-left {
|
| 497 |
+
flex-direction: column;
|
| 498 |
+
align-items: stretch;
|
| 499 |
+
gap: 12px;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.theme-selector {
|
| 503 |
+
justify-content: space-between;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.scale-controls {
|
| 507 |
+
justify-content: space-between;
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
</style>
|
app/src/components/trackio/core/adaptive-sampler.js
CHANGED
|
@@ -7,24 +7,24 @@
|
|
| 7 |
export class AdaptiveSampler {
|
| 8 |
constructor(options = {}) {
|
| 9 |
this.options = {
|
| 10 |
-
maxPoints: 400, //
|
| 11 |
-
targetPoints: 200, //
|
| 12 |
-
preserveFeatures: true, //
|
| 13 |
adaptiveStrategy: 'smart', // 'uniform', 'smart', 'lod'
|
| 14 |
-
smoothingWindow: 3, //
|
| 15 |
...options
|
| 16 |
};
|
| 17 |
}
|
| 18 |
|
| 19 |
/**
|
| 20 |
-
*
|
| 21 |
*/
|
| 22 |
needsSampling(dataLength) {
|
| 23 |
return dataLength > this.options.maxPoints;
|
| 24 |
}
|
| 25 |
|
| 26 |
/**
|
| 27 |
-
*
|
| 28 |
*/
|
| 29 |
sampleSeries(data, strategy = null) {
|
| 30 |
if (!Array.isArray(data) || data.length === 0) {
|
|
@@ -234,11 +234,11 @@ export class AdaptiveSampler {
|
|
| 234 |
const index = Math.floor(logProgress * (totalLength - 1));
|
| 235 |
indices.push(Math.max(1, Math.min(totalLength - 2, index)));
|
| 236 |
}
|
| 237 |
-
return [...new Set(indices)]; //
|
| 238 |
}
|
| 239 |
|
| 240 |
/**
|
| 241 |
-
*
|
| 242 |
*/
|
| 243 |
sampleByVariation(data, targetPoints) {
|
| 244 |
const variations = [];
|
|
@@ -290,7 +290,7 @@ export class AdaptiveSampler {
|
|
| 290 |
* Reconstruit les données complètes pour une zone spécifique (pour le zoom)
|
| 291 |
*/
|
| 292 |
getFullDataForRange(originalData, samplingInfo, startStep, endStep) {
|
| 293 |
-
//
|
| 294 |
// quand l'utilisateur zoom sur une zone spécifique
|
| 295 |
const startIdx = originalData.findIndex(d => d.step >= startStep);
|
| 296 |
const endIdx = originalData.findIndex(d => d.step > endStep);
|
|
|
|
| 7 |
export class AdaptiveSampler {
|
| 8 |
constructor(options = {}) {
|
| 9 |
this.options = {
|
| 10 |
+
maxPoints: 400, // Threshold to trigger sampling
|
| 11 |
+
targetPoints: 200, // Target number of points after sampling
|
| 12 |
+
preserveFeatures: true, // Preserve important peaks/valleys
|
| 13 |
adaptiveStrategy: 'smart', // 'uniform', 'smart', 'lod'
|
| 14 |
+
smoothingWindow: 3, // Window for feature detection
|
| 15 |
...options
|
| 16 |
};
|
| 17 |
}
|
| 18 |
|
| 19 |
/**
|
| 20 |
+
* Determine if sampling is necessary
|
| 21 |
*/
|
| 22 |
needsSampling(dataLength) {
|
| 23 |
return dataLength > this.options.maxPoints;
|
| 24 |
}
|
| 25 |
|
| 26 |
/**
|
| 27 |
+
* Main entry point for sampling
|
| 28 |
*/
|
| 29 |
sampleSeries(data, strategy = null) {
|
| 30 |
if (!Array.isArray(data) || data.length === 0) {
|
|
|
|
| 234 |
const index = Math.floor(logProgress * (totalLength - 1));
|
| 235 |
indices.push(Math.max(1, Math.min(totalLength - 2, index)));
|
| 236 |
}
|
| 237 |
+
return [...new Set(indices)]; // Remove duplicates
|
| 238 |
}
|
| 239 |
|
| 240 |
/**
|
| 241 |
+
* Sampling based on local variation
|
| 242 |
*/
|
| 243 |
sampleByVariation(data, targetPoints) {
|
| 244 |
const variations = [];
|
|
|
|
| 290 |
* Reconstruit les données complètes pour une zone spécifique (pour le zoom)
|
| 291 |
*/
|
| 292 |
getFullDataForRange(originalData, samplingInfo, startStep, endStep) {
|
| 293 |
+
// This method would allow recovering more details
|
| 294 |
// quand l'utilisateur zoom sur une zone spécifique
|
| 295 |
const startIdx = originalData.findIndex(d => d.step >= startStep);
|
| 296 |
const endIdx = originalData.findIndex(d => d.step > endStep);
|
app/src/content/embeds/banner.html
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-robot-arm" style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;"></div>
|
| 2 |
+
<script>
|
| 3 |
+
(() => {
|
| 4 |
+
const ensureD3 = (cb) => {
|
| 5 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 6 |
+
let s = document.getElementById('d3-cdn-script');
|
| 7 |
+
if (!s) {
|
| 8 |
+
s = document.createElement('script');
|
| 9 |
+
s.id = 'd3-cdn-script';
|
| 10 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 11 |
+
document.head.appendChild(s);
|
| 12 |
+
}
|
| 13 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 14 |
+
s.addEventListener('load', onReady, { once: true });
|
| 15 |
+
if (window.d3) onReady();
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const bootstrap = () => {
|
| 19 |
+
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
|
| 20 |
+
const container = (mount && mount.querySelector && mount.querySelector('.d3-robot-arm')) || document.querySelector('.d3-robot-arm');
|
| 21 |
+
if (!container) return;
|
| 22 |
+
if (container.dataset) {
|
| 23 |
+
if (container.dataset.mounted === 'true') return;
|
| 24 |
+
container.dataset.mounted = 'true';
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Robot arm parameters
|
| 28 |
+
const armLengths = [120, 100, 80]; // 3 segments
|
| 29 |
+
const numSegments = armLengths.length;
|
| 30 |
+
|
| 31 |
+
// Trail for end effector
|
| 32 |
+
const trailLength = 80;
|
| 33 |
+
const trail = [];
|
| 34 |
+
|
| 35 |
+
// Animation state
|
| 36 |
+
let targetX = 0, targetY = 0;
|
| 37 |
+
let currentX = 0, currentY = 0; // Smooth interpolated position
|
| 38 |
+
let time = 0;
|
| 39 |
+
let prevPositions = null;
|
| 40 |
+
|
| 41 |
+
// Task system
|
| 42 |
+
let currentTask = 0;
|
| 43 |
+
let taskProgress = 0;
|
| 44 |
+
let gripperOpen = true;
|
| 45 |
+
let gripperOpenness = 1.0; // 0 = closed, 1 = open (for smooth animation)
|
| 46 |
+
let heldObject = null;
|
| 47 |
+
|
| 48 |
+
// Objects to manipulate with initial and target positions
|
| 49 |
+
const objects = [
|
| 50 |
+
{ id: 1, x: 0, y: 0, targetX: 0, targetY: 0, size: 25, color: 0.2, label: 'Cube A', shape: 'square', placed: false },
|
| 51 |
+
{ id: 2, x: 0, y: 0, targetX: 0, targetY: 0, size: 20, color: 0.5, label: 'Ball B', shape: 'circle', placed: false },
|
| 52 |
+
{ id: 3, x: 0, y: 0, targetX: 0, targetY: 0, size: 22, color: 0.8, label: 'Cube C', shape: 'square', placed: false }
|
| 53 |
+
];
|
| 54 |
+
|
| 55 |
+
// Task sequences optimized for smooth reach
|
| 56 |
+
const tasks = [
|
| 57 |
+
{ type: 'pick', objectId: 1, duration: 100 },
|
| 58 |
+
{ type: 'move', x: 0.3, y: 0.5, duration: 90 },
|
| 59 |
+
{ type: 'place', duration: 60 },
|
| 60 |
+
{ type: 'return', duration: 80 },
|
| 61 |
+
{ type: 'pick', objectId: 2, duration: 100 },
|
| 62 |
+
{ type: 'move', x: 0.5, y: 0.6, duration: 90 },
|
| 63 |
+
{ type: 'place', duration: 60 },
|
| 64 |
+
{ type: 'return', duration: 80 },
|
| 65 |
+
{ type: 'pick', objectId: 3, duration: 100 },
|
| 66 |
+
{ type: 'move', x: 0.7, y: 0.5, duration: 90 },
|
| 67 |
+
{ type: 'place', duration: 60 },
|
| 68 |
+
{ type: 'idle', duration: 100 }
|
| 69 |
+
];
|
| 70 |
+
|
| 71 |
+
// FABRIK Inverse Kinematics solver (Forward And Backward Reaching)
|
| 72 |
+
const solveIK = (targetX, targetY, baseX, baseY) => {
|
| 73 |
+
// Initialize joint positions (use previous if available, otherwise extend horizontally)
|
| 74 |
+
let positions;
|
| 75 |
+
|
| 76 |
+
if (prevPositions && prevPositions.length === numSegments + 1) {
|
| 77 |
+
// Use previous positions as starting point for smooth animation
|
| 78 |
+
positions = prevPositions.map(p => ({ x: p.x, y: p.y }));
|
| 79 |
+
positions[0] = { x: baseX, y: baseY }; // Always fix base
|
| 80 |
+
} else {
|
| 81 |
+
// Initial position: extend arm horizontally to the right
|
| 82 |
+
positions = [{ x: baseX, y: baseY }];
|
| 83 |
+
let x = baseX, y = baseY;
|
| 84 |
+
for (let i = 0; i < numSegments; i++) {
|
| 85 |
+
x += armLengths[i];
|
| 86 |
+
positions.push({ x, y });
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// FABRIK iterations
|
| 91 |
+
const maxIterations = 10;
|
| 92 |
+
const tolerance = 0.1;
|
| 93 |
+
|
| 94 |
+
for (let iter = 0; iter < maxIterations; iter++) {
|
| 95 |
+
// Forward reaching: start from end effector
|
| 96 |
+
positions[numSegments].x = targetX;
|
| 97 |
+
positions[numSegments].y = targetY;
|
| 98 |
+
|
| 99 |
+
for (let i = numSegments - 1; i >= 0; i--) {
|
| 100 |
+
const dx = positions[i].x - positions[i + 1].x;
|
| 101 |
+
const dy = positions[i].y - positions[i + 1].y;
|
| 102 |
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
| 103 |
+
|
| 104 |
+
if (dist > 0) {
|
| 105 |
+
const lambda = armLengths[i] / dist;
|
| 106 |
+
positions[i].x = positions[i + 1].x + dx * lambda;
|
| 107 |
+
positions[i].y = positions[i + 1].y + dy * lambda;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Backward reaching: start from base
|
| 112 |
+
positions[0].x = baseX;
|
| 113 |
+
positions[0].y = baseY;
|
| 114 |
+
|
| 115 |
+
for (let i = 0; i < numSegments; i++) {
|
| 116 |
+
const dx = positions[i + 1].x - positions[i].x;
|
| 117 |
+
const dy = positions[i + 1].y - positions[i].y;
|
| 118 |
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
| 119 |
+
|
| 120 |
+
if (dist > 0) {
|
| 121 |
+
const lambda = armLengths[i] / dist;
|
| 122 |
+
positions[i + 1].x = positions[i].x + dx * lambda;
|
| 123 |
+
positions[i + 1].y = positions[i].y + dy * lambda;
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Check convergence
|
| 128 |
+
const endDx = positions[numSegments].x - targetX;
|
| 129 |
+
const endDy = positions[numSegments].y - targetY;
|
| 130 |
+
const endDist = Math.sqrt(endDx * endDx + endDy * endDy);
|
| 131 |
+
|
| 132 |
+
if (endDist < tolerance) break;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Calculate angles from positions
|
| 136 |
+
const angles = [];
|
| 137 |
+
for (let i = 0; i < numSegments; i++) {
|
| 138 |
+
const dx = positions[i + 1].x - positions[i].x;
|
| 139 |
+
const dy = positions[i + 1].y - positions[i].y;
|
| 140 |
+
angles.push(Math.atan2(dy, dx));
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Save positions for next iteration
|
| 144 |
+
prevPositions = positions;
|
| 145 |
+
|
| 146 |
+
return { angles, positions };
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
// Colors
|
| 150 |
+
const c0 = d3.rgb(78, 165, 183);
|
| 151 |
+
const c1 = d3.rgb(206, 192, 250);
|
| 152 |
+
const c2 = d3.rgb(232, 137, 171);
|
| 153 |
+
const interp01 = d3.interpolateRgb(c0, c1);
|
| 154 |
+
const interp12 = d3.interpolateRgb(c1, c2);
|
| 155 |
+
const colorFor = (v) => {
|
| 156 |
+
const t = Math.max(0, Math.min(1, v));
|
| 157 |
+
return t <= 0.5 ? interp01(t / 0.5) : interp12((t - 0.5) / 0.5);
|
| 158 |
+
};
|
| 159 |
+
|
| 160 |
+
const svg = d3.select(container).append('svg')
|
| 161 |
+
.attr('width', '100%')
|
| 162 |
+
.style('display', 'block')
|
| 163 |
+
.style('cursor', 'default');
|
| 164 |
+
|
| 165 |
+
const render = () => {
|
| 166 |
+
const width = container.clientWidth || 800;
|
| 167 |
+
const height = Math.max(260, Math.round(width / 3));
|
| 168 |
+
svg.attr('width', width).attr('height', height);
|
| 169 |
+
|
| 170 |
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 171 |
+
const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
|
| 172 |
+
const glowColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)';
|
| 173 |
+
|
| 174 |
+
// Base position (left side, bottom)
|
| 175 |
+
const baseX = width * 0.15;
|
| 176 |
+
const baseY = height * 0.85;
|
| 177 |
+
|
| 178 |
+
// Max reach of arm (sum of all segments)
|
| 179 |
+
const maxReach = armLengths.reduce((a, b) => a + b, 0); // 300px
|
| 180 |
+
|
| 181 |
+
// Initialize object positions (only once) - on table surface, within reach
|
| 182 |
+
if (objects[0].x === 0) {
|
| 183 |
+
const tableY = baseY - 20; // Same as table surface
|
| 184 |
+
|
| 185 |
+
objects[0].x = baseX + 130;
|
| 186 |
+
objects[0].y = tableY - objects[0].size / 2 - 4;
|
| 187 |
+
objects[0].targetX = objects[0].x;
|
| 188 |
+
objects[0].targetY = objects[0].y;
|
| 189 |
+
|
| 190 |
+
objects[1].x = baseX + 200;
|
| 191 |
+
objects[1].y = tableY - objects[1].size / 2 - 4;
|
| 192 |
+
objects[1].targetX = objects[1].x;
|
| 193 |
+
objects[1].targetY = objects[1].y;
|
| 194 |
+
|
| 195 |
+
objects[2].x = baseX + 270;
|
| 196 |
+
objects[2].y = tableY - objects[2].size / 2 - 4;
|
| 197 |
+
objects[2].targetX = objects[2].x;
|
| 198 |
+
objects[2].targetY = objects[2].y;
|
| 199 |
+
|
| 200 |
+
currentX = baseX + 180;
|
| 201 |
+
currentY = baseY - 150;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// Task execution system
|
| 205 |
+
const task = tasks[currentTask % tasks.length];
|
| 206 |
+
taskProgress++;
|
| 207 |
+
|
| 208 |
+
if (taskProgress >= task.duration) {
|
| 209 |
+
taskProgress = 0;
|
| 210 |
+
currentTask++;
|
| 211 |
+
if (currentTask >= tasks.length) {
|
| 212 |
+
currentTask = 0;
|
| 213 |
+
// Reset objects to initial positions on table
|
| 214 |
+
const tableY = baseY - 20;
|
| 215 |
+
|
| 216 |
+
objects[0].x = baseX + 130;
|
| 217 |
+
objects[0].y = tableY - objects[0].size / 2 - 4;
|
| 218 |
+
objects[0].targetX = objects[0].x;
|
| 219 |
+
objects[0].targetY = objects[0].y;
|
| 220 |
+
objects[0].placed = false;
|
| 221 |
+
|
| 222 |
+
objects[1].x = baseX + 200;
|
| 223 |
+
objects[1].y = tableY - objects[1].size / 2 - 4;
|
| 224 |
+
objects[1].targetX = objects[1].x;
|
| 225 |
+
objects[1].targetY = objects[1].y;
|
| 226 |
+
objects[1].placed = false;
|
| 227 |
+
|
| 228 |
+
objects[2].x = baseX + 270;
|
| 229 |
+
objects[2].y = tableY - objects[2].size / 2 - 4;
|
| 230 |
+
objects[2].targetX = objects[2].x;
|
| 231 |
+
objects[2].targetY = objects[2].y;
|
| 232 |
+
objects[2].placed = false;
|
| 233 |
+
|
| 234 |
+
trail.length = 0;
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
const t = taskProgress / task.duration; // 0 to 1
|
| 239 |
+
// Easing function for smooth movement
|
| 240 |
+
const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
| 241 |
+
const smoothT = easeInOutCubic(t);
|
| 242 |
+
|
| 243 |
+
// Execute current task with smooth transitions
|
| 244 |
+
if (task.type === 'pick') {
|
| 245 |
+
const obj = objects.find(o => o.id === task.objectId);
|
| 246 |
+
if (obj) {
|
| 247 |
+
// Approach object smoothly - hover above it
|
| 248 |
+
targetX = obj.x;
|
| 249 |
+
targetY = obj.y - 20; // Hover well above object
|
| 250 |
+
|
| 251 |
+
// Smooth gripper closing
|
| 252 |
+
if (t > 0.6) {
|
| 253 |
+
gripperOpenness = Math.max(0, 1 - (t - 0.6) / 0.3);
|
| 254 |
+
if (t > 0.75 && gripperOpen) {
|
| 255 |
+
gripperOpen = false;
|
| 256 |
+
heldObject = obj;
|
| 257 |
+
}
|
| 258 |
+
} else {
|
| 259 |
+
gripperOpenness = 1.0;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
} else if (task.type === 'move') {
|
| 263 |
+
// Move to placement position (within reach, above table)
|
| 264 |
+
const destX = baseX + 150 + task.x * 100; // Spread across reachable area
|
| 265 |
+
const destY = baseY - 80 - task.y * 60; // Stay above table, within reach
|
| 266 |
+
targetX = destX;
|
| 267 |
+
targetY = destY;
|
| 268 |
+
} else if (task.type === 'place') {
|
| 269 |
+
// Open gripper smoothly
|
| 270 |
+
if (t > 0.2) {
|
| 271 |
+
gripperOpenness = Math.min(1, (t - 0.2) / 0.4);
|
| 272 |
+
if (t > 0.4 && !gripperOpen) {
|
| 273 |
+
gripperOpen = true;
|
| 274 |
+
if (heldObject) {
|
| 275 |
+
heldObject.targetX = targetX;
|
| 276 |
+
heldObject.targetY = targetY + 20;
|
| 277 |
+
heldObject.placed = true;
|
| 278 |
+
heldObject = null;
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
} else if (task.type === 'return') {
|
| 283 |
+
targetX = baseX + 180;
|
| 284 |
+
targetY = baseY - 140;
|
| 285 |
+
gripperOpenness = 1.0;
|
| 286 |
+
} else if (task.type === 'idle') {
|
| 287 |
+
targetX = baseX + 180;
|
| 288 |
+
targetY = baseY - 140;
|
| 289 |
+
gripperOpenness = 1.0;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Smooth interpolation of current position towards target
|
| 293 |
+
const lerpFactor = 0.12; // Smoothing factor
|
| 294 |
+
currentX += (targetX - currentX) * lerpFactor;
|
| 295 |
+
currentY += (targetY - currentY) * lerpFactor;
|
| 296 |
+
|
| 297 |
+
// Solve IK using smoothed position
|
| 298 |
+
const { angles, positions: joints } = solveIK(currentX, currentY, baseX, baseY);
|
| 299 |
+
|
| 300 |
+
// Update trail
|
| 301 |
+
const endEffector = joints[joints.length - 1];
|
| 302 |
+
trail.push({ x: endEffector.x, y: endEffector.y });
|
| 303 |
+
if (trail.length > trailLength) trail.shift();
|
| 304 |
+
|
| 305 |
+
// Smooth object positions towards their targets
|
| 306 |
+
objects.forEach(obj => {
|
| 307 |
+
const objLerpFactor = 0.15;
|
| 308 |
+
obj.x += (obj.targetX - obj.x) * objLerpFactor;
|
| 309 |
+
obj.y += (obj.targetY - obj.y) * objLerpFactor;
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
// Update held object position (stick to gripper)
|
| 313 |
+
if (heldObject) {
|
| 314 |
+
heldObject.x = endEffector.x;
|
| 315 |
+
heldObject.y = endEffector.y + 8;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// Ensure container can host tooltip
|
| 319 |
+
container.style.position = container.style.position || 'relative';
|
| 320 |
+
let tip = container.querySelector('.d3-tooltip');
|
| 321 |
+
let tipInner;
|
| 322 |
+
if (!tip) {
|
| 323 |
+
tip = document.createElement('div');
|
| 324 |
+
tip.className = 'd3-tooltip';
|
| 325 |
+
Object.assign(tip.style, {
|
| 326 |
+
position: 'absolute',
|
| 327 |
+
top: '0px',
|
| 328 |
+
left: '0px',
|
| 329 |
+
transform: 'translate(-9999px, -9999px)',
|
| 330 |
+
pointerEvents: 'none',
|
| 331 |
+
padding: '10px 12px',
|
| 332 |
+
borderRadius: '12px',
|
| 333 |
+
fontSize: '12px',
|
| 334 |
+
lineHeight: '1.35',
|
| 335 |
+
border: '1px solid var(--border-color)',
|
| 336 |
+
background: 'var(--surface-bg)',
|
| 337 |
+
color: 'var(--text-color)',
|
| 338 |
+
boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',
|
| 339 |
+
opacity: '0',
|
| 340 |
+
transition: 'opacity .12s ease',
|
| 341 |
+
backdropFilter: 'saturate(1.12) blur(8px)',
|
| 342 |
+
zIndex: '20'
|
| 343 |
+
});
|
| 344 |
+
tipInner = document.createElement('div');
|
| 345 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 346 |
+
Object.assign(tipInner.style, {
|
| 347 |
+
textAlign: 'left',
|
| 348 |
+
display: 'flex',
|
| 349 |
+
flexDirection: 'column',
|
| 350 |
+
gap: '6px',
|
| 351 |
+
minWidth: '220px'
|
| 352 |
+
});
|
| 353 |
+
tip.appendChild(tipInner);
|
| 354 |
+
container.appendChild(tip);
|
| 355 |
+
} else {
|
| 356 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Draw workspace table/surface
|
| 360 |
+
const workspaceGroup = svg.selectAll('g.workspace').data([0]).join('g').attr('class', 'workspace');
|
| 361 |
+
|
| 362 |
+
// Table surface
|
| 363 |
+
workspaceGroup.selectAll('rect.table').data([0]).join('rect')
|
| 364 |
+
.attr('class', 'table')
|
| 365 |
+
.attr('x', baseX + 100)
|
| 366 |
+
.attr('y', baseY - 20)
|
| 367 |
+
.attr('width', 200)
|
| 368 |
+
.attr('height', 8)
|
| 369 |
+
.attr('fill', isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)')
|
| 370 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)')
|
| 371 |
+
.attr('stroke-width', 1)
|
| 372 |
+
.attr('rx', 2);
|
| 373 |
+
|
| 374 |
+
// Draw robot base/body
|
| 375 |
+
const bodyGroup = svg.selectAll('g.robot-body').data([0]).join('g').attr('class', 'robot-body');
|
| 376 |
+
|
| 377 |
+
// Base platform
|
| 378 |
+
bodyGroup.selectAll('rect.base').data([0]).join('rect')
|
| 379 |
+
.attr('class', 'base')
|
| 380 |
+
.attr('x', baseX - 60)
|
| 381 |
+
.attr('y', baseY - 10)
|
| 382 |
+
.attr('width', 120)
|
| 383 |
+
.attr('height', 30)
|
| 384 |
+
.attr('fill', colorFor(0.1))
|
| 385 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)')
|
| 386 |
+
.attr('stroke-width', 2)
|
| 387 |
+
.attr('rx', 4);
|
| 388 |
+
|
| 389 |
+
// Vertical pillar
|
| 390 |
+
bodyGroup.selectAll('rect.pillar').data([0]).join('rect')
|
| 391 |
+
.attr('class', 'pillar')
|
| 392 |
+
.attr('x', baseX - 15)
|
| 393 |
+
.attr('y', baseY - 40)
|
| 394 |
+
.attr('width', 30)
|
| 395 |
+
.attr('height', 40)
|
| 396 |
+
.attr('fill', colorFor(0.15))
|
| 397 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)')
|
| 398 |
+
.attr('stroke-width', 2)
|
| 399 |
+
.attr('rx', 3);
|
| 400 |
+
|
| 401 |
+
// Draw trail (lighter, behind everything)
|
| 402 |
+
const trailGroup = svg.selectAll('g.trail').data([0]).join('g').attr('class', 'trail');
|
| 403 |
+
const trailPath = d3.line()
|
| 404 |
+
.x(d => d.x)
|
| 405 |
+
.y(d => d.y)
|
| 406 |
+
.curve(d3.curveCatmullRom.alpha(0.5));
|
| 407 |
+
|
| 408 |
+
trailGroup.selectAll('path').data([trail]).join('path')
|
| 409 |
+
.attr('d', trailPath)
|
| 410 |
+
.attr('fill', 'none')
|
| 411 |
+
.attr('stroke', colorFor(0.7))
|
| 412 |
+
.attr('stroke-width', 1.5)
|
| 413 |
+
.attr('stroke-opacity', 0.25)
|
| 414 |
+
.attr('stroke-linecap', 'round');
|
| 415 |
+
|
| 416 |
+
// Draw arm segments
|
| 417 |
+
const armGroup = svg.selectAll('g.arm').data([0]).join('g').attr('class', 'arm');
|
| 418 |
+
|
| 419 |
+
armGroup.selectAll('line.segment').data(d3.range(numSegments)).join('line')
|
| 420 |
+
.attr('class', 'segment')
|
| 421 |
+
.attr('x1', i => joints[i].x)
|
| 422 |
+
.attr('y1', i => joints[i].y)
|
| 423 |
+
.attr('x2', i => joints[i + 1].x)
|
| 424 |
+
.attr('y2', i => joints[i + 1].y)
|
| 425 |
+
.attr('stroke', (d, i) => colorFor(i / (numSegments - 1)))
|
| 426 |
+
.attr('stroke-width', (d, i) => 8 - i * 1.5)
|
| 427 |
+
.attr('stroke-linecap', 'round')
|
| 428 |
+
.attr('stroke-opacity', 0.9);
|
| 429 |
+
|
| 430 |
+
// Draw objects to manipulate
|
| 431 |
+
const objectsGroup = svg.selectAll('g.objects').data([0]).join('g').attr('class', 'objects');
|
| 432 |
+
|
| 433 |
+
objectsGroup.selectAll('.object').data(objects).join(
|
| 434 |
+
enter => {
|
| 435 |
+
const g = enter.append('g').attr('class', 'object');
|
| 436 |
+
g.each(function(d) {
|
| 437 |
+
const elem = d3.select(this);
|
| 438 |
+
if (d.shape === 'square') {
|
| 439 |
+
elem.append('rect')
|
| 440 |
+
.attr('class', 'obj-shape')
|
| 441 |
+
.attr('width', d.size)
|
| 442 |
+
.attr('height', d.size)
|
| 443 |
+
.attr('rx', 3);
|
| 444 |
+
} else {
|
| 445 |
+
elem.append('circle')
|
| 446 |
+
.attr('class', 'obj-shape')
|
| 447 |
+
.attr('r', d.size / 2);
|
| 448 |
+
}
|
| 449 |
+
});
|
| 450 |
+
return g;
|
| 451 |
+
},
|
| 452 |
+
update => update
|
| 453 |
+
)
|
| 454 |
+
.attr('transform', d => `translate(${d.x}, ${d.y})`)
|
| 455 |
+
.style('cursor', 'pointer')
|
| 456 |
+
.each(function(d) {
|
| 457 |
+
const shape = d3.select(this).select('.obj-shape');
|
| 458 |
+
if (d.shape === 'square') {
|
| 459 |
+
shape
|
| 460 |
+
.attr('x', -d.size / 2)
|
| 461 |
+
.attr('y', -d.size / 2)
|
| 462 |
+
.attr('fill', colorFor(d.color))
|
| 463 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.3)')
|
| 464 |
+
.attr('stroke-width', 2);
|
| 465 |
+
} else {
|
| 466 |
+
shape
|
| 467 |
+
.attr('fill', colorFor(d.color))
|
| 468 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.3)')
|
| 469 |
+
.attr('stroke-width', 2);
|
| 470 |
+
}
|
| 471 |
+
})
|
| 472 |
+
.on('mouseenter', function(ev, d) {
|
| 473 |
+
d3.select(this).select('.obj-shape')
|
| 474 |
+
.transition().duration(120)
|
| 475 |
+
.attr('transform', 'scale(1.1)');
|
| 476 |
+
tipInner.innerHTML =
|
| 477 |
+
`<div style="font-weight:800;"><strong>${d.label}</strong></div>` +
|
| 478 |
+
`<div style="font-size:11px;color:var(--muted-color);margin-top:-2px;">Object ${d.shape === 'square' ? '(Cube)' : '(Sphere)'}</div>` +
|
| 479 |
+
`<div style="padding-top:4px;border-top:1px solid var(--border-color);"><strong>Position</strong> X ${d.x.toFixed(0)} · Y ${d.y.toFixed(0)}</div>` +
|
| 480 |
+
`<div><strong>Status</strong> ${heldObject === d ? 'Grasped' : 'On table'}</div>`;
|
| 481 |
+
tip.style.opacity = '1';
|
| 482 |
+
})
|
| 483 |
+
.on('mousemove', (ev) => {
|
| 484 |
+
const [mx, my] = d3.pointer(ev, container);
|
| 485 |
+
tip.style.transform = `translate(${Math.round(mx + 10)}px, ${Math.round(my + 12)}px)`;
|
| 486 |
+
})
|
| 487 |
+
.on('mouseleave', function() {
|
| 488 |
+
d3.select(this).select('.obj-shape')
|
| 489 |
+
.transition().duration(120)
|
| 490 |
+
.attr('transform', 'scale(1)');
|
| 491 |
+
tip.style.opacity = '0';
|
| 492 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 493 |
+
});
|
| 494 |
+
|
| 495 |
+
// Draw gripper (pincers at end effector) with smooth animation
|
| 496 |
+
const gripperGroup = svg.selectAll('g.gripper').data([0]).join('g').attr('class', 'gripper');
|
| 497 |
+
const gripperAngle = angles[numSegments - 1];
|
| 498 |
+
const gripperSize = 8 + gripperOpenness * 10; // 8px (closed) to 18px (open)
|
| 499 |
+
|
| 500 |
+
gripperGroup.selectAll('line.gripper-jaw').data([1, -1]).join('line')
|
| 501 |
+
.attr('class', 'gripper-jaw')
|
| 502 |
+
.attr('x1', endEffector.x)
|
| 503 |
+
.attr('y1', endEffector.y)
|
| 504 |
+
.attr('x2', d => endEffector.x + Math.cos(gripperAngle + Math.PI / 2 * d) * gripperSize)
|
| 505 |
+
.attr('y2', d => endEffector.y + Math.sin(gripperAngle + Math.PI / 2 * d) * gripperSize)
|
| 506 |
+
.attr('stroke', colorFor(0.9))
|
| 507 |
+
.attr('stroke-width', 4)
|
| 508 |
+
.attr('stroke-linecap', 'round')
|
| 509 |
+
.style('transition', 'all 0.1s ease');
|
| 510 |
+
|
| 511 |
+
// Draw joints
|
| 512 |
+
const jointGroup = svg.selectAll('g.joints').data([0]).join('g').attr('class', 'joints');
|
| 513 |
+
|
| 514 |
+
jointGroup.selectAll('circle.joint').data(joints.slice(0, -1)).join('circle') // Don't draw last joint (gripper is there)
|
| 515 |
+
.attr('class', 'joint')
|
| 516 |
+
.attr('cx', d => d.x)
|
| 517 |
+
.attr('cy', d => d.y)
|
| 518 |
+
.attr('r', (d, i) => i === 0 ? 12 : (i === joints.length - 1 ? 10 : 8))
|
| 519 |
+
.attr('fill', (d, i) => i === 0 ? colorFor(0) : (i === joints.length - 1 ? colorFor(1) : colorFor(i / joints.length)))
|
| 520 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)')
|
| 521 |
+
.attr('stroke-width', 2)
|
| 522 |
+
.style('cursor', 'pointer')
|
| 523 |
+
.on('mouseenter', function(ev, d, i) {
|
| 524 |
+
const idx = joints.indexOf(d);
|
| 525 |
+
d3.select(this)
|
| 526 |
+
.raise()
|
| 527 |
+
.style('filter', `drop-shadow(0 0 12px ${glowColor})`)
|
| 528 |
+
.transition().duration(120).ease(d3.easeCubicOut)
|
| 529 |
+
.attr('r', (idx === 0 ? 12 : (idx === joints.length - 1 ? 10 : 8)) * 1.3)
|
| 530 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
|
| 531 |
+
.attr('stroke-width', 3);
|
| 532 |
+
|
| 533 |
+
const jointName = idx === 0 ? 'Base Joint' : `Joint ${idx}`;
|
| 534 |
+
const angle = idx > 0 ? angles[idx - 1] * (180 / Math.PI) : 0;
|
| 535 |
+
|
| 536 |
+
tipInner.innerHTML =
|
| 537 |
+
`<div style="font-weight:800;letter-spacing:.1px;"><strong>${jointName}</strong></div>` +
|
| 538 |
+
`<div style="font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;letter-spacing:.1px;">Robot Arm Joint</div>` +
|
| 539 |
+
`<div style="padding-top:6px;border-top:1px solid var(--border-color);"><strong>Position</strong> X ${d.x.toFixed(1)} · <strong>Y</strong> ${d.y.toFixed(1)}</div>` +
|
| 540 |
+
(idx > 0 ? `<div><strong>Angle</strong> ${angle.toFixed(1)}°</div>` : '') +
|
| 541 |
+
`<div><strong>Current Task</strong> ${task.type}</div>`;
|
| 542 |
+
tip.style.opacity = '1';
|
| 543 |
+
})
|
| 544 |
+
.on('mousemove', (ev) => {
|
| 545 |
+
const [mx, my] = d3.pointer(ev, container);
|
| 546 |
+
tip.style.transform = `translate(${Math.round(mx + 10)}px, ${Math.round(my + 12)}px)`;
|
| 547 |
+
})
|
| 548 |
+
.on('mouseleave', function(ev, d) {
|
| 549 |
+
const idx = joints.indexOf(d);
|
| 550 |
+
tip.style.opacity = '0';
|
| 551 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 552 |
+
d3.select(this)
|
| 553 |
+
.style('filter', null)
|
| 554 |
+
.transition().duration(120).ease(d3.easeCubicOut)
|
| 555 |
+
.attr('r', idx === 0 ? 12 : (idx === joints.length - 1 ? 10 : 8))
|
| 556 |
+
.attr('stroke', isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)')
|
| 557 |
+
.attr('stroke-width', 2);
|
| 558 |
+
});
|
| 559 |
+
|
| 560 |
+
// Draw task status label
|
| 561 |
+
const statusGroup = svg.selectAll('g.status').data([0]).join('g').attr('class', 'status');
|
| 562 |
+
|
| 563 |
+
const taskText = task.type === 'pick' ? `Picking ${objects.find(o => o.id === task.objectId)?.label}` :
|
| 564 |
+
task.type === 'move' ? 'Moving object' :
|
| 565 |
+
task.type === 'place' ? 'Placing object' :
|
| 566 |
+
task.type === 'return' ? 'Returning to home' : 'Idle';
|
| 567 |
+
|
| 568 |
+
statusGroup.selectAll('text.task-label').data([taskText]).join('text')
|
| 569 |
+
.attr('class', 'task-label')
|
| 570 |
+
.attr('x', width - 20)
|
| 571 |
+
.attr('y', 30)
|
| 572 |
+
.attr('text-anchor', 'end')
|
| 573 |
+
.attr('font-size', '14px')
|
| 574 |
+
.attr('font-weight', '600')
|
| 575 |
+
.attr('fill', colorFor(0.5))
|
| 576 |
+
.attr('opacity', 0.8)
|
| 577 |
+
.text(d => d);
|
| 578 |
+
|
| 579 |
+
// Draw progress bar
|
| 580 |
+
statusGroup.selectAll('rect.progress-bg').data([0]).join('rect')
|
| 581 |
+
.attr('class', 'progress-bg')
|
| 582 |
+
.attr('x', width - 200)
|
| 583 |
+
.attr('y', 40)
|
| 584 |
+
.attr('width', 180)
|
| 585 |
+
.attr('height', 6)
|
| 586 |
+
.attr('fill', isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)')
|
| 587 |
+
.attr('rx', 3);
|
| 588 |
+
|
| 589 |
+
statusGroup.selectAll('rect.progress-fill').data([0]).join('rect')
|
| 590 |
+
.attr('class', 'progress-fill')
|
| 591 |
+
.attr('x', width - 200)
|
| 592 |
+
.attr('y', 40)
|
| 593 |
+
.attr('width', 180 * t)
|
| 594 |
+
.attr('height', 6)
|
| 595 |
+
.attr('fill', colorFor(0.5))
|
| 596 |
+
.attr('rx', 3);
|
| 597 |
+
};
|
| 598 |
+
|
| 599 |
+
// Animation loop
|
| 600 |
+
let animationFrame;
|
| 601 |
+
const animate = () => {
|
| 602 |
+
render();
|
| 603 |
+
animationFrame = requestAnimationFrame(animate);
|
| 604 |
+
};
|
| 605 |
+
|
| 606 |
+
// Resize handling
|
| 607 |
+
if (window.ResizeObserver) {
|
| 608 |
+
const ro = new ResizeObserver(() => {
|
| 609 |
+
// render() is already called in animate loop
|
| 610 |
+
});
|
| 611 |
+
ro.observe(container);
|
| 612 |
+
} else {
|
| 613 |
+
window.addEventListener('resize', () => {
|
| 614 |
+
// render() is already called in animate loop
|
| 615 |
+
});
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
// Start animation
|
| 619 |
+
animate();
|
| 620 |
+
|
| 621 |
+
// Cleanup on unmount
|
| 622 |
+
const cleanup = () => {
|
| 623 |
+
if (animationFrame) cancelAnimationFrame(animationFrame);
|
| 624 |
+
};
|
| 625 |
+
if (container.dataset) container.dataset.cleanup = cleanup;
|
| 626 |
+
};
|
| 627 |
+
|
| 628 |
+
if (document.readyState === 'loading') {
|
| 629 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 630 |
+
} else { ensureD3(bootstrap); }
|
| 631 |
+
})();
|
| 632 |
+
</script>
|
| 633 |
+
|
app/src/content/{embeds2 → embeds}/d3-bar.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-benchmark.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-confusion-matrix.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-evals-after-fix.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-evals-tpbug.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-line-quad.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-line.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-matrix.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-neural-network.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-pie-quad.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-pie.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/d3-scatter.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/demo/color-picker.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/demo/content-structure.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/demo/palettes.html
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/original_embeds/plotly/banner.py
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/original_embeds/plotly/bar.py
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/original_embeds/plotly/heatmap.py
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/original_embeds/plotly/line.py
RENAMED
|
File without changes
|
app/src/content/{embeds2 → embeds}/original_embeds/plotly/poetry.lock
RENAMED
|
File without changes
|