diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/README.md b/packages/preview/unofficial-sorbonne-presentation/0.1.0/README.md
new file mode 100644
index 0000000000..98733223ff
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/README.md
@@ -0,0 +1,175 @@
+# Unofficial Sorbonne Presentation Theme (Community Edition)
+
+A community-built, non-official structured presentation theme designed to match the branding and style guidelines of the Sorbonne University, built on top of the [presentate](https://typst.app/universe/package/presentate) and [navigator](https://typst.app/universe/package/navigator) packages.
+
+---
+
+## Overview
+
+The theme provides academic and institutional slide decks that respect the visual identity of Sorbonne University while offering powerful dynamic features.
+
+- **Faculty Presets**: Built-in colors and logos for Health, Science, Humanities, and University-wide presentations.
+- **Smart Navigation**: Automatic breadcrumbs, transition slides with roadmaps, and flexible hierarchy mapping.
+- **Dynamic Content**: Seamless integration of `pause`, `uncover`, and `only` for step-by-step reveals.
+- **Scientific Ready**: Specialized slides for equations, figures, and algorithms.
+
+### 🎨 Faculty Presets
+Switch visual identities instantly using the `faculty` parameter:
+
+| Univ | Sante | Sciences | Lettres |
+|:---:|:---:|:---:|:---:|
+|  |  |  |  |
+
+- `univ`: Sorbonne Blue (University-wide)
+- `sante`: Sorbonne Red (Faculty of Health)
+- `sciences`: Sorbonne Light Blue (Faculty of Science & Engineering)
+- `lettres`: Sorbonne Yellow/Ocre (Faculty of Humanities)
+
+## Logo Customization
+
+You can easily replace the default institutional logos with your own by using the following parameters in the `template` function.
+
+### `logo-transition` (Monochrome/White version)
+This logo is used on **solid theme-colored backgrounds**. For best results, use a white or high-contrast monochrome version of your logo.
+It appears on:
+- The **Title Slide** (bottom right).
+- All **Transition Slides** (Parts, Sections, Subsections) in the top left.
+- **Focus Slides** and the **Ending Slide**.
+
+### `logo-slide` (Color version)
+This logo is used on **standard slides** (white background). Use your full-color logo here.
+It appears in the **Header** of every content slide, next to the title.
+
+**Example:**
+```typ
+#show: template.with(
+ logo-slide: "assets/my-logo-color.png",
+ logo-transition: "assets/my-logo-white.png",
+)
+```
+
+## Documentation
+
+For a comprehensive visual tour of all components and features, please refer to the pre-compiled PDF documentation:
+
+- **[Main Demo Guide](examples/demo.typ)** : All components, boxes, and slide types.
+- **[2-Levels Mapping Guide](examples/demo-mapping-2levels.typ)** : Using Section/Subsection hierarchy.
+- **[3-Levels Mapping Guide](examples/demo-mapping-3levels.typ)** : Using Part/Section/Subsection hierarchy.
+
+## Quick Start
+
+```typ
+#import "@preview/unofficial-sorbonne-presentation:0.1.0": *
+
+#show: template.with(
+ title: [Scientific Discovery],
+ author: [John Doe],
+ faculty: "sciences",
+ show-outline: true,
+)
+
+= Introduction
+#slide[
+ - High performance
+ - Intuitive syntax
+ #show: pause
+ - *Dynamic* animations
+]
+
+#ending-slide()
+```
+
+## Configuration Reference
+
+### The `template` function
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `title` | content | `none` | Main presentation title |
+| `short-title` | content | `none` | Short version of title for footer |
+| `subtitle` | content | `none` | Optional subtitle |
+| `author` | content | `none` | Presenter's name |
+| `short-author` | content | `none` | Short version of author for footer |
+| `affiliation` | content | `none` | Department or Laboratory |
+| `date` | content | `datetime...` | Custom date display |
+| `faculty` | string | `"sante"` | Preset: `"sante"`, `"sciences"`, `"lettres"`, `"univ"` |
+| `primary-color` | color | `none` | Manual override for theme color |
+| `alert-color` | color | `none` | Manual override for alert text color |
+| `logo-slide` | string | `none` | Path to custom logo for content slides |
+| `logo-transition` | string | `none` | Path to custom logo for transition slides |
+| `text-font` | string | `"Fira Sans"` | Main font family |
+| `text-size` | length | `20pt` | Base text size |
+| `aspect-ratio` | string | `"16-9"` | `"16-9"` or `"4-3"` |
+| `show-outline` | bool | `false` | Toggle summary slide |
+| `outline-title` | content | `[Sommaire]` | Title of the summary slide |
+| `outline-depth` | int | `2` | Levels shown in summary |
+| `outline-columns` | int | `1` | Number of columns for summary |
+| `mapping` | dict | `(sec: 1, sub: 2)` | Logic mapping for headings |
+| `auto-title` | bool | `true` | Use section name as slide title if none provided |
+| `show-header-numbering` | bool | `true` | Toggle all heading numbers |
+| `numbering-format` | string | `"1.1"` | Format for sections and subsections |
+| `part-numbering-format` | string | `"I"` | Format for parts |
+| `annex-title` | content | `[Annexe]` | Prefix for single appendix |
+| `annex-main-title` | content | `[Annexes]` | Focus slide text for appendix start |
+| `annex-numbering-format` | string | `"I"` | Numbering style for appendices |
+| `bib-style` | string | `"apa"` | Bibliography and citation style |
+| `progress-bar` | string | `"none"` | Position: `"none"`, `"top"`, or `"bottom"` |
+| `slide-break-suffix` | content | `[ (cont.)]` | Suffix appended to titles on broken slides |
+| `footer-author` | bool | `true` | Toggle author display in footer |
+| `footer-title` | bool | `true` | Toggle title display in footer |
+| `max-length` | int \| dict | `none` | Max length for breadcrumb titles before truncation |
+
+## Component Reference
+
+### Slide Types
+- `#slide(title: none, subtitle: none, allow-slide-breaks: false, background: none, body)`: Standard content slide.
+ - `allow-slide-breaks`: If `true`, allows content to overflow onto multiple slides. A suffix (defined by `slide-break-suffix`) is automatically appended to the title from the second page. *Note: This feature is incompatible with dynamic animations like `#pause`.*
+ - `background`: Optional content (e.g., an image) to display behind the slide content.
+ 
+- `#slide-break()`: Manually forces a page break within a slide. *Note: Only works when `allow-slide-breaks: true` is set on the `#slide()`.*
+- `#focus-slide(body, subtitle: none)`: Highlight slide on solid theme background.
+ 
+- `#figure-slide(fig, title: none, subtitle: none, caption: none, ..)`: Centered figure slide.
+ 
+- `#equation-slide(equation, title: none, subtitle: none, definitions: none, citation: none, ..)`: Large equation with "signature" citation.
+ 
+- `#acknowledgement-slide(title: none, subtitle: none, people: (), institutions: (), ..)`: Thank-you slide.
+ 
+- `#ending-slide(title: none, subtitle: none, contact: ())`: Closing slide with contact information.
+ 
+
+### Text Helpers
+- `#alert[text]`: Highlighted bold text.
+- `#muted[text]`: Gray secondary text.
+- `#subtle[text]`: Light gray tertiary text.
+
+
+
+### Citations & References
+- Inline and corner citations.
+ `#cite-box("smith2023", position: "bottom-right")`
+ 
+
+### Layout & Boxes
+- `#two-col(left, right, columns: (1fr, 1fr), gutter: 2em)`: Balanced columns.
+ 
+- `#three-col(left, center, right, ..)`: Three column layout.
+ 
+- `#grid-2x2(tl, tr, bl, br, ..)`: Four-quadrant grid layout.
+ 
+- **Boxes**: All boxes support the `fill-mode` parameter (`"outline"`, `"fill"`, `"full"`, or `"transparent"`).
+ 
+ - `#highlight-box(title, body)`: Blue university-styled box for key points.
+ - `#alert-box(title, body)`: Red cautionary box for warnings.
+ - `#example-box(title, body)`: Green academic box for examples.
+ - `#algorithm-box(title, body)`: Monospace box for algorithmic logic.
+ - `#themed-block(title, body)`: Box automatically matching the faculty color.
+
+## Credits
+
+- **Underlying Packages**: Built with [presentate](https://typst.app/universe/package/presentate) and [navigator](https://typst.app/universe/package/navigator).
+- **Inspiration**: Layout features and component designs were inspired by the [calmly-touying](https://typst.app/universe/package/calmly-touying) theme. A special thanks to its author for the high-quality design inspiration.
+
+## License
+
+MIT License. See [LICENSE](LICENSE) for details.
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-acknowledgement-slide.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-acknowledgement-slide.png
new file mode 100644
index 0000000000..89f583a913
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-acknowledgement-slide.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-boxes.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-boxes.png
new file mode 100644
index 0000000000..77af63492f
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-boxes.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-citation.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-citation.png
new file mode 100644
index 0000000000..e72a60c708
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-citation.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-ending-slide.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-ending-slide.png
new file mode 100644
index 0000000000..31cc88e80e
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-ending-slide.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-equation-slide.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-equation-slide.png
new file mode 100644
index 0000000000..835ec55f00
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-equation-slide.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-figure-slide.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-figure-slide.png
new file mode 100644
index 0000000000..abebf30f8b
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-figure-slide.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-focus-slide.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-focus-slide.png
new file mode 100644
index 0000000000..20dfbc824b
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-focus-slide.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-slide.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-slide.png
new file mode 100644
index 0000000000..4285778265
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/component-slide.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-lettres.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-lettres.png
new file mode 100644
index 0000000000..b8d5d4af00
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-lettres.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-sante.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-sante.png
new file mode 100644
index 0000000000..dc8fe7ae79
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-sante.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-sciences.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-sciences.png
new file mode 100644
index 0000000000..f276acb402
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-sciences.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-univ.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-univ.png
new file mode 100644
index 0000000000..3224573357
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/faculty-univ.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/helper-text.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/helper-text.png
new file mode 100644
index 0000000000..5e2e44121f
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/helper-text.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-2col.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-2col.png
new file mode 100644
index 0000000000..76e35013dc
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-2col.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-3col.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-3col.png
new file mode 100644
index 0000000000..7613cca440
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-3col.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-grid2x2.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-grid2x2.png
new file mode 100644
index 0000000000..04ca16062a
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/docs/layout-grid2x2.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-lettres-white.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-lettres-white.svg
new file mode 100644
index 0000000000..4350922598
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-lettres-white.svg
@@ -0,0 +1,123 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-lettres.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-lettres.svg
new file mode 100644
index 0000000000..a874a5210f
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-lettres.svg
@@ -0,0 +1,123 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sante-white.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sante-white.svg
new file mode 100644
index 0000000000..75ed77126e
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sante-white.svg
@@ -0,0 +1,107 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sante.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sante.svg
new file mode 100644
index 0000000000..9159d1b86c
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sante.svg
@@ -0,0 +1,107 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sciences-white.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sciences-white.svg
new file mode 100644
index 0000000000..49bac66a2e
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sciences-white.svg
@@ -0,0 +1,108 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sciences.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sciences.svg
new file mode 100644
index 0000000000..e029c3717e
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-sciences.svg
@@ -0,0 +1,108 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-univ-white.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-univ-white.svg
new file mode 100644
index 0000000000..eb773db5ff
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-univ-white.svg
@@ -0,0 +1,84 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-univ.svg b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-univ.svg
new file mode 100644
index 0000000000..5373a2fa05
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/assets/logo/logo-univ.svg
@@ -0,0 +1,84 @@
+
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo-mapping-2levels.typ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo-mapping-2levels.typ
new file mode 100644
index 0000000000..f17bf5f810
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo-mapping-2levels.typ
@@ -0,0 +1,43 @@
+#import "@preview/unofficial-sorbonne-presentation:0.1.0": *
+
+#show: template.with(
+ title: [Standard Mapping Guide],
+ subtitle: [Section & Subsection Hierarchy],
+ author: [David Hajage],
+ // Standard mapping: Level 1 (=) is a Section, Level 2 (==) is a Subsection
+ mapping: (section: 1, subsection: 2),
+ // Numbering format for sections and subsections
+ numbering-format: "1.1",
+ show-outline: true,
+)
+
+= Introduction
+== Description
+#slide[
+ In this configuration:
+ - Heading level 1 (`=`) acts as a *Section*.
+ - Heading level 2 (`==`) acts as a *Subsection*.
+]
+
+== Roadmap
+#slide[
+ Section transitions will display a "Roadmap" (mini table of contents) listing all the subsections within that section.
+
+ Subsections (Level 2) are displayed in the roadmap of the parent section transition slide.
+
+ They also appear in the breadcrumb at the bottom of the slide.
+]
+
+= Technical Details
+
+== Implementation
+#slide(title: "The numbering-format option")[
+ The `numbering-format` parameter controls how sections and subsections are numbered.
+
+ For example, `numbering-format: "1.1"` will produce:
+ - *1.* for the first section.
+ - *1.1* for the first subsection.
+]
+#slide[
+ You can change it to `"1.a"` or `"I.1"` depending on your preferences.
+]
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo-mapping-3levels.typ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo-mapping-3levels.typ
new file mode 100644
index 0000000000..246549eb8c
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo-mapping-3levels.typ
@@ -0,0 +1,46 @@
+#import "@preview/unofficial-sorbonne-presentation:0.1.0": *
+
+#show: template.with(
+ title: [Complex Mapping Guide],
+ subtitle: [Part, Section & Subsection Hierarchy],
+ author: [David Hajage],
+ // Complex mapping: 3 levels of hierarchy
+ mapping: (part: 1, section: 2, subsection: 3),
+ // Numbering for the Part (Level 1)
+ part-numbering-format: "I",
+ // Numbering for Sections (Level 2) and Subsections (Level 3)
+ numbering-format: "1.a",
+ show-outline: true,
+)
+
+#slide[
+ When a level is mapped to `part`:
+ - The transition slide is centered and "quiet" (no roadmap).
+ - It usually represents a major thematic block.
+ - The numbering follows `part-numbering-format` (here: "I").
+]
+
+= First Part
+== Introduction Section
+=== Context
+#slide[
+ In this 3-level setup:
+ - Level 1 (`=`) is a *Part*.
+ - Level 2 (`==`) is a *Section*.
+ - Level 3 (`===`) is a *Subsection*.
+
+ The Section transition (Level 2) will show a roadmap of all Subsections (Level 3) within it.
+]
+
+=== Problem Statement
+#slide[
+ Look at the breadcrumb: it now tracks three levels of depth.
+]
+
+= Second Part
+== Results Section
+=== Data Analysis
+#slide[
+ The `numbering-format` starts from the Section level.
+ Here, `numbering-format: "1.a"` means sections are "1", "2", and subsections are "1.a", "1.b".
+]
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo.typ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo.typ
new file mode 100644
index 0000000000..96d9e1e6b1
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/demo.typ
@@ -0,0 +1,291 @@
+#import "@preview/unofficial-sorbonne-presentation:0.1.0": *
+#import "@preview/physica:0.9.8": *
+
+#show: template.with(
+ title: [(Unofficial) Sorbonne Template: Complete Guide],
+ subtitle: [Demonstration of all features and components],
+ short-title: [Template guide],
+ short-author: [D. Hajage],
+ author: [David Hajage],
+ affiliation: [Example University],
+ faculty: "univ",
+ show-outline: true,
+ mapping: (section: 1),
+)
+
+// ==========================================
+= Basic Components & Layout
+// ==========================================
+
+#slide(title: "Slides & Lists")[
+ The `#slide()` function is the core component. It supports standard Typst content.
+
+ + First item
+ + Second item
+ - Sub-item
+]
+
+#slide(title: "Text Styles")[
+ Three functions to prioritize information:
+
+ - *Alert*: For #alert[critical] information.
+ - *Muted*: For #muted[secondary] information.
+ - *Subtle*: For #subtle[tertiary] information.
+]
+
+#slide(title: "Multi-column Layouts", allow-slide-breaks: true)[
+ The template provides dedicated functions for balanced layouts:
+
+ - `two-col(left, right, ..)` and `three-col(left, center, right, ..)`
+ - *Parameters*:
+ - `columns`: Array of widths (e.g., `(1fr, 2fr)`). Defaults to equal widths.
+ - `gutter`: Spacing between columns (default: `2em`).
+
+ #v(0.5em)
+ #two-col(
+ [*Two-col*: Default equal width. #lorem(5)],
+ [#lorem(10)]
+ )
+ #v(0.5em)
+ #two-col(
+ [*Custom width*: Using `columns: (1fr, 2fr)`.], [#lorem(10)],
+ columns: (1fr, 2fr)
+ )
+ #v(0.5em)
+ #three-col(
+ [*Three-col*], [Equal width], [distribution]
+ )
+ #v(0.5em)
+ #grid-2x2(
+ [*Grid-2x2*], [Top Right],
+ [Bottom Left], [Bottom Right]
+ )
+]
+
+#slide(title: "Automatic Page Breaks", allow-slide-breaks: true)[
+ When a slide contains too much content (like a long list or a bibliography), you can use `allow-slide-breaks: true`.
+
+ - Content flows naturally to the next physical slide.
+ - Headers and footers are automatically repeated.
+ - A suffix (default: " (cont.)") is added to the title from the 2nd page.
+
+ *Long List Example:*
+ #for i in range(1, 16) [
+ + Item number #i
+ ]
+]
+
+#slide(title: "Manual slide Breaks", allow-slide-breaks: true)[
+ You can also force a break manually using `#slide-break()`.
+
+ - This is the first part of the slide.
+ - Useful for logically separating long content.
+
+ #slide-break()
+
+ - This is the second part, after a manual break.
+ - The title is automatically suffixed with "(cont.)".
+]
+
+#slide(subtitle: "Demonstrating auto-title with manual subtitle")[
+ This slide has no manual `title` parameter.
+
+ Because `auto-title` is `true` (default), it automatically uses the name of the current section ("Basic Components & Layout") as the title, while displaying the provided `subtitle` below it.
+]
+
+#slide(
+ title: "Slide with Background",
+ background: block(width: 100%, height: 100%, {
+ place(center + horizon, image("../assets/logo/logo-univ.svg", width: 40%))
+ place(top + left, rect(fill: white.transparentize(50%), width: 100%, height: 100%))
+ })
+)[
+ You can add a background to any slide using the `background` parameter.
+
+ In this example, we use the university logo with a semi-transparent white overlay to ensure content readability.
+]
+
+// ==========================================
+= Boxes & Blocks
+// ==========================================
+
+#slide(title: "Institutional Boxes")[
+ #highlight-box(title: "Highlight Box")[Key points using theme blue.]
+ #v(0.5em)
+ #alert-box(title: "Alert Box", fill-mode: "fill")[Warnings using theme red.]
+ #v(0.5em)
+ #example-box(title: "Example Box", fill-mode: "full")[Examples using green.]
+]
+
+#slide(title: "Technical Blocks")[
+ #algorithm-box(title: "Algorithm Box")[
+ + Step 1: Initialize
+ + Step 2: Process
+ ]
+ #v(1em)
+ #themed-block(title: "Themed Block")[Adapts to the chosen faculty color.]
+]
+
+// ==========================================
+= Citations & References
+// ==========================================
+
+#slide(title: "Citations Style")[
+ Inline citations like @smith2023 or @einstein1905 are highlighted.
+
+ You can also use corner boxes:
+
+ #cite-box("smith2023", position: "bottom-right")
+ #cite-box("doe2024", display-label: "Jane Doe (2024)", position: "top-right")
+
+ _Note: The citation style can be customized via the `bib-style` parameter (default: "apa")._
+]
+
+#slide(title: "Bibliography Slide")[
+ The bibliography is standard and should be placed in a `#slide()`.
+
+ #bibliography("refs.bib", title: none)
+]
+
+// ==========================================
+= Special Slide Types
+// ==========================================
+
+#focus-slide[
+ This is a `#focus-slide` for impactful messages.
+]
+
+#figure-slide(
+ rect(width: 40%, height: 30%, fill: sorbonne-lightblue),
+ title: "Figure Slide",
+ caption: [A centered caption]
+)
+
+#acknowledgement-slide(
+ subtitle: [Special thanks to my supervisor:],
+ people: ((name: "Prof. Smith", role: "Supervisor"),),
+ institutions: ("Example University",),
+)
+
+#equation-slide(
+ $ i hbar pdv(Psi, t) = - hbar^2 / (2m) laplacian(Psi) + V Psi $,
+ title: [Equation Slide],
+ definitions: [
+ / $Psi$: Wavefunction
+ / $V$: Potential energy
+ ],
+ citation: (bib-key: "einstein1905", label: "Quantum Origins")
+)
+
+// ==========================================
+= Dynamic Features
+// ==========================================
+
+#slide(title: "Sequential Reveal with pause")[
+ The `presentate` package allows for step-by-step reveals:
+
+ - Point 1: Always visible.
+ #show: pause
+ - Point 2: Appears after a click.
+ #show: pause
+ - Point 3: Final point.
+
+]
+
+#slide(title: "Precise Control: only and uncover")[
+ You can control exactly which subslide an element appears on:
+
+ #two-col(
+ [
+ #uncover(2, 3, 4)[Visible from step 2.] \
+ #uncover(3, 4)[Visible only at step 3.]
+ ],
+ [
+ #only(1)[Step 1 content.]
+ #only(2)[Step 2 content.]
+ #only(3, 4)[Step 3 content.]
+ ]
+ )
+
+ #uncover(4)[
+ #v(1em)
+ #alert-box(title: "Important Limitation")[
+ Dynamic animations are *incompatible* with the `allow-slide-breaks: true` option.
+ ]
+ ]
+
+]
+
+// ==========================================
+= Template Configuration
+// ==========================================
+
+#slide(title: "Appendix & Hierarchy Control")[
+ #two-col(
+ [
+ *Using Appendices*
+ - Call `#appendix()` to start.
+ - Resets heading counters.
+ - Displays a focus slide using `annex-main-title`.
+ - Changes numbering style to `annex-title` + `annex-numbering-format`.
+ ],
+ [
+ *Mapping Logic*
+ - `mapping` defines roles for levels:
+ - `(section: 1)` : Level 1 is a section.
+ - `(part: 1, section: 2)` : Level 1 is a Part, Level 2 is a Section.
+ - Transition slides and breadcrumbs adapt to these roles.
+ ]
+ )
+]
+
+#slide(title: "Theme Configuration Reference (1/2)")[
+ #set text(size: 0.72em)
+ #two-col(
+ [
+ *Identification & Date*
+ - `title`, `short-title`, `subtitle`.
+ - `author`, `short-author`, `affiliation`.
+ - `date`: Defaults to today.
+
+ *Visual Identity*
+ - `faculty`: `"univ"` (default), `"sante"`, `"sciences"`, `"lettres"`.
+ - `primary-color` / `alert-color`: Manual hex/rgb overrides.
+ - `logo-slide` / `logo-transition`: Image paths.
+
+ *Typography & Global*
+ - `text-font` / `text-size`: e.g., `"Fira Sans"`, `20pt`.
+ - `aspect-ratio`: `"16-9"` or `"4-3"`.
+ ],
+ [
+ *Outline (TOC)*
+ - `show-outline`: Toggle summary slide.
+ - `outline-title`: Title of the TOC.
+ - `outline-depth`: Levels shown in TOC.
+ - `outline-columns`: Number of columns for TOC.
+
+ *Header & Numbering*
+ - `show-header-numbering`: Toggle all numbers.
+ - `numbering-format`: For sections (e.g., `"1.1"`).
+ - `part-numbering-format`: For parts (e.g., `"I"`).
+ ]
+ )
+]
+
+#slide(title: "Theme Configuration Reference (2/2)")[
+ #set text(size: 0.8em)
+ *Navigation & Appendix*
+ - `mapping`: Dict of roles (part/section/subsection) vs levels.
+ - `auto-title`: Boolean. If true, slides without a title use the section name.
+ - `transitions`: Dictionary for `navigator` roadmap customization.
+ - `bib-style`: Bibliography style (default: `"apa"`).
+ - `annex-title`: Prefix for single appendix (e.g., `"Appendix"`).
+ - `annex-main-title`: Focus slide text (e.g., `"Technical Annexes"`).
+ - `annex-numbering-format`: Numbering style (e.g., `"A"`, `"I"`, `"1"`).
+ - `progress-bar`: Position of the bar (`"none"`, `"top"`, `"bottom"`).
+ - `slide-break-suffix`: Suffix for broken slides (default: `" (cont.)"`).
+ - `footer-author` / `footer-title`: Boolean toggles for footer info.
+ - `max-length`: (`int` or `dict`) Truncate breadcrumb titles. Ex: `20` or `(section: 10, subsection: 20)`.
+]
+
+#ending-slide()
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/refs.bib b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/refs.bib
new file mode 100644
index 0000000000..419a6e63fb
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/examples/refs.bib
@@ -0,0 +1,22 @@
+@article{smith2023,
+ title={Improvements in Typst Templates},
+ author={Smith, John},
+ journal={Journal of Typesetting},
+ year={2023}
+}
+
+@book{doe2024,
+ title={The Art of Presentations},
+ author={Doe, Jane},
+ publisher={Sorbonne Press},
+ year={2024}
+}
+
+@article{einstein1905,
+ title={Does the Inertia of a Body Depend Upon Its Energy Content?},
+ author={Einstein, Albert},
+ journal={Annalen der Physik},
+ volume={18},
+ pages={639--641},
+ year={1905}
+}
\ No newline at end of file
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/lib.typ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/lib.typ
new file mode 100644
index 0000000000..22b7c815d5
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/lib.typ
@@ -0,0 +1,2 @@
+#import "sorbonne.typ": template, slide, focus-slide, figure-slide, figure-slide-split, acknowledgement-slide, equation-slide, ending-slide, cite-box, alert, muted, subtle, appendix, slide-break, two-col, three-col, grid-2x2, highlight-box, alert-box, example-box, algorithm-box, themed-block, sorbonne-red, sorbonne-blue, sorbonne-lightblue, sorbonne-yellow, sorbonne-text
+#import "@preview/presentate:0.2.4": pause, uncover, only, fragments, step-item
\ No newline at end of file
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/sorbonne.typ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/sorbonne.typ
new file mode 100644
index 0000000000..0aebd3a655
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/sorbonne.typ
@@ -0,0 +1,857 @@
+#import "@preview/presentate:0.2.4" as p
+#import p.store: states
+// On importe la version locale modifiée de navigator
+#import "@preview/navigator:0.1.3" as nav
+
+// --- Configuration et Couleurs ---
+#let sorbonne-red = rgb("#AC182E")
+#let sorbonne-blue = rgb("#1D2769")
+#let sorbonne-lightblue = rgb("#52B5E5")
+#let sorbonne-yellow = rgb("#FFB700")
+#let sorbonne-text = rgb("#263068")
+
+// État pour la configuration du thème
+#let config-state = state("sorbonne-config", none)
+#let last-main-page = state("last-main-page", none)
+#let logical-slide-counter = counter("sorbonne-logical-slide")
+
+// --- Composants ---
+
+#let progress-bar-line() = context {
+ let conf = config-state.get()
+ if conf == none or conf.progress-bar == "none" { return none }
+
+ let current = logical-slide-counter.get().at(0)
+ let appendix-marker = query()
+ let total = if appendix-marker.len() > 0 {
+ logical-slide-counter.at(appendix-marker.first().location()).at(0) - 1
+ } else {
+ logical-slide-counter.final().at(0)
+ }
+
+ if total == 0 { return none }
+
+ let is-annex = if appendix-marker.len() > 0 {
+ appendix-marker.first().location().page() <= here().page()
+ } else { false }
+
+ let ratio = if is-annex { 1.0 } else { calc.min(1.0, current / total) }
+
+ block(width: 100% * ratio, height: 2pt, fill: conf.primary-color)
+}
+
+#let empty-slide(fill: none, body) = {
+ set page(margin: 0pt, fill: fill, header: none, footer: none, foreground: none)
+ [
+ #logical-slide-counter.step()
+ #p.slide(logical-slide: true, {
+ [#metadata((title: none, subtitle: none, allow-slide-breaks: false)) ]
+ body
+ })
+ ]
+}
+
+#let breadcrumb() = context {
+ let conf = config-state.get()
+ if conf == none { return none }
+ set text(size: 0.8em, fill: gray.darken(20%))
+
+ let mapping = conf.mapping
+ let level-modes = (:)
+ for role in ("part", "section", "subsection") {
+ let lvl = mapping.at(role, default: none)
+ if lvl != none { level-modes.insert("level-" + str(lvl) + "-mode", "current") }
+ }
+
+ nav.progressive-outline(
+ ..level-modes,
+ layout: "horizontal",
+ separator: text(fill: gray.lighten(50%), " / "),
+ clickable: false,
+ )
+}
+
+#let sorbonne-header() = context {
+ let conf = config-state.get()
+ if conf == none { return none }
+
+ let markers = query()
+ if markers.len() == 0 { return none }
+
+ // On cherche le marqueur qui s'applique à cette page.
+ // C'est soit un marqueur sur cette page, soit le dernier marqueur avant.
+ let current-page = here().page()
+ let marker = markers.filter(m => m.location().page() <= current-page).last()
+ let h = marker.value
+
+ // On n'affiche (suite) que si allow-slide-breaks est activé ET qu'on est sur une page physique suivante
+ let allow-breaks = if type(h) == dictionary { h.at("allow-slide-breaks", default: false) } else { false }
+ let is-continuation = current-page > marker.location().page() and allow-breaks
+
+ let resolved-title = if type(h) == dictionary and h.title != none { h.title } else { nav.resolve-slide-title(none) }
+ if resolved-title == none and h.subtitle == none { return none }
+
+ let title-display = if is-continuation and resolved-title != none {
+ resolved-title + text(size: 0.8em, weight: "regular", fill: sorbonne-text.lighten(40%), conf.slide-break-suffix)
+ } else {
+ resolved-title
+ }
+
+ block(width: 100%, inset: (x: 2em, top: 0.8em, bottom: 0.2em), {
+ grid(
+ columns: (4.5em, 1fr),
+ column-gutter: 1.5em,
+ align: horizon,
+ image(conf.logo-slide, width: 4.5em),
+ stack(dir: ttb, spacing: 0.3em,
+ if resolved-title != none {
+ text(size: 1.1em, weight: "bold", fill: sorbonne-text, smallcaps(title-display))
+ },
+ if h.subtitle != none {
+ text(size: 0.85em, style: "italic", fill: sorbonne-text.lighten(20%), h.subtitle)
+ }
+ )
+ )
+ })
+}
+
+#let sorbonne-footer() = context {
+ let conf = config-state.get()
+ if conf == none { return none }
+
+ block(width: 100%, inset: (x: 2.5em, bottom: 0.8em, top: 0.2em), {
+ set text(size: 0.65em, fill: gray.darken(20%))
+ line(length: 100%, stroke: 0.5pt + gray.lighten(80%))
+ v(0.5em)
+
+ let show-author = conf.footer-author
+ let show-title = conf.footer-title
+
+ if not show-author and not show-title {
+ grid(
+ columns: (1fr, auto),
+ align: (left, right),
+ breadcrumb(),
+ context {
+ let current = logical-slide-counter.get().at(0)
+ let appendix-marker = query()
+ let total = if appendix-marker.len() > 0 {
+ logical-slide-counter.at(appendix-marker.first().location()).at(0) - 1
+ } else {
+ logical-slide-counter.final().at(0)
+ }
+ [#current / #total]
+ }
+ )
+ } else {
+ grid(
+ columns: (1fr, 1fr, 1fr),
+ align: (left, center, right),
+ text(size: 0.9em, weight: "regular", {
+ if show-author { conf.short-author }
+ if show-author and show-title [ #h(0.5em) · #h(0.5em) ]
+ if show-title { conf.short-title }
+ }),
+ breadcrumb(),
+ context {
+ let current = logical-slide-counter.get().at(0)
+ let appendix-marker = query()
+ let total = if appendix-marker.len() > 0 {
+ logical-slide-counter.at(appendix-marker.first().location()).at(0) - 1
+ } else {
+ logical-slide-counter.final().at(0)
+ }
+ [#current / #total]
+ }
+ )
+ }
+ })
+}
+
+#let apply-layout(breakable: true, body) = context {
+ let config = config-state.get()
+ set text(font: config.text-font, size: config.text-size, fill: sorbonne-text)
+
+ if not breakable {
+ // Le grid 1fr occupe tout l'espace disponible, permettant le centrage vertical (horizon)
+ // tout en s'ajustant si des notes de bas de page sont présentes.
+ grid(
+ columns: 100%,
+ rows: 1fr,
+ inset: (x: 2.5em, top: 0.5em, bottom: 0pt),
+ {
+ metadata((t: "ContentSlide"))
+ body
+ }
+ )
+ } else {
+ block(width: 100%, breakable: true, inset: (x: 2.5em, top: 0.5em, bottom: 0pt), {
+ metadata((t: "ContentSlide"))
+ body
+ })
+ }
+}
+
+#let focus-slide(body, subtitle: none) = context {
+ let conf = config-state.get()
+ empty-slide(fill: conf.primary-color, {
+ place(top + left, pad(top: 2em, left: 2em, image(conf.logo-transition, width: 5em)))
+ set text(fill: white, weight: "bold")
+ align(center + horizon, stack(dir: ttb, spacing: 1em,
+ text(size: 2.5em, body),
+ if subtitle != none {
+ text(size: 1.5em, weight: "regular", style: "italic", fill: white.transparentize(20%), subtitle)
+ }
+ ))
+ })
+}
+
+#let alert(body) = context {
+ let conf = config-state.get()
+ text(fill: conf.alert-color, weight: "bold", body)
+}
+
+#let muted(body) = text(fill: gray, body)
+
+#let subtle(body) = text(fill: gray.lighten(40%), body)
+
+#let two-col(left, right, columns: (1fr, 1fr), gutter: 2em) = {
+ grid(
+ columns: columns,
+ column-gutter: gutter,
+ left,
+ right
+ )
+}
+
+#let three-col(left, center, right, columns: (1fr, 1fr, 1fr), gutter: 2em) = {
+ grid(
+ columns: columns,
+ column-gutter: gutter,
+ left,
+ center,
+ right
+ )
+}
+
+#let grid-2x2(tl, tr, bl, br, columns: (1fr, 1fr), rows: (auto, auto), gutter: 2em) = {
+ grid(
+ columns: columns,
+ rows: rows,
+ column-gutter: gutter,
+ row-gutter: gutter,
+ tl,
+ tr,
+ bl,
+ br
+ )
+}
+
+// --- API ---
+
+#let slide(..args) = {
+ let pos = args.pos()
+ let named = args.named()
+ let manual-title = named.at("title", default: none)
+ let subtitle = named.at("subtitle", default: none)
+ let allow-slide-breaks = named.at("allow-slide-breaks", default: false)
+ let background = named.at("background", default: none)
+ let body = if pos.len() > 0 { pos.at(0) } else { none }
+
+ let clean-named = named
+ for key in ("title", "subtitle", "allow-slide-breaks", "background") {
+ if key in clean-named {
+ let _ = clean-named.remove(key)
+ }
+ }
+
+ [
+ #logical-slide-counter.step()
+ #p.slide(..clean-named, {
+ if background != none {
+ place(top + left, dx: 0pt, dy: -4.5em, block(width: 100%, height: 100% + 4.5em + 3.0em, background))
+ }
+ [#metadata((title: manual-title, subtitle: subtitle, allow-slide-breaks: allow-slide-breaks)) ]
+ apply-layout(breakable: allow-slide-breaks, body)
+ })
+ ]
+}
+
+#let figure-slide(fig, title: none, subtitle: none, caption: none, ..args) = {
+ slide(title: title, subtitle: subtitle, ..args, {
+ set align(center + horizon)
+ figure(fig, caption: caption)
+ })
+}
+
+#let figure-slide-split(fig-left, fig-right, title: none, subtitle: none, caption-left: none, caption-right: none, ..args) = {
+ slide(title: title, subtitle: subtitle, ..args, {
+ set align(center + horizon)
+ grid(
+ columns: (1fr, 1fr),
+ column-gutter: 2em,
+ figure(fig-left, caption: caption-left),
+ figure(fig-right, caption: caption-right)
+ )
+ })
+}
+
+#let acknowledgement-slide(
+ title: "Acknowledgements",
+ subtitle: none,
+ people: (),
+ institutions: (),
+ extra: none,
+ ..args
+) = {
+ slide(title: title, subtitle: subtitle, ..args, {
+ set align(center + horizon)
+ stack(
+ dir: ttb,
+ spacing: 1.5em,
+
+ if people.len() > 0 {
+ align(center, grid(
+ columns: (auto, auto),
+ column-gutter: 2em,
+ row-gutter: 1em,
+ ..people.map(p => (
+ align(right, text(weight: "bold", p.name)),
+ align(left, p.role)
+ )).flatten()
+ ))
+ },
+
+ if institutions.len() > 0 {
+ v(0.5em)
+ align(center, institutions.join([ #h(2em) ]))
+ },
+
+ if extra != none {
+ v(1em)
+ extra
+ }
+ )
+ })
+}
+
+#let cite-box(bib-key, display-label: none, position: "bottom-right", form: "normal") = context {
+ let conf = config-state.get()
+
+ let align-pos = if position == "top-right" { top + right }
+ else if position == "bottom-left" { bottom + left }
+ else { bottom + right }
+
+ let dx = if "right" in position { 1em } else { -1em }
+ let dy = if "top" in position { -0.3em } else { 0.3em }
+
+ let keys = if type(bib-key) == array { bib-key } else if bib-key != none { (bib-key,) } else { () }
+ let labels = keys.map(k => if type(k) == str { label(k) } else { k })
+
+ let content = if display-label != none {
+ // On "cite" de manière invisible pour forcer l'inclusion en bibliographie
+ if labels.len() > 0 {
+ place(hide(labels.map(l => cite(l, form: form)).join()))
+ }
+ display-label
+ } else if labels.len() > 0 {
+ labels.map(l => cite(l, form: form)).join(", ")
+ } else {
+ none
+ }
+
+ if content != none {
+ place(align-pos, dx: dx, dy: dy, block(
+ fill: conf.primary-color.lighten(95%),
+ stroke: 0.5pt + conf.primary-color,
+ radius: 3pt,
+ inset: 0.4em,
+ text(size: 0.65em, fill: conf.primary-color, content)
+ ))
+ }
+}
+
+#let equation-slide(
+ equation,
+ title: "Equation",
+ subtitle: none,
+ definitions: none,
+ citation: none,
+ ..args
+) = {
+ slide(title: title, subtitle: subtitle, ..args, {
+ set align(center + horizon)
+
+ stack(
+ dir: ttb,
+ spacing: 2em,
+
+ // Bloc Equation + Signature
+ stack(
+ dir: ttb,
+ spacing: 0.8em,
+ block(
+ text(size: 2.5em, weight: "bold", equation)
+ ),
+ if citation != none {
+ let key = if type(citation) == dictionary { citation.at("bib-key", default: none) } else { citation }
+ let lbl = if type(citation) == dictionary { citation.at("label", default: none) } else { none }
+ let keys = if type(key) == array { key } else if key != none { (key,) } else { () }
+ let labels = keys.map(k => if type(k) == str { label(k) } else { k })
+
+ if labels.len() > 0 { place(hide(labels.map(l => cite(l)).join())) }
+
+ let cite-content = if lbl != none { lbl } else { labels.map(l => cite(l)).join(", ") }
+
+ // Style "Signature" : Ã droite, en gris, avec tiret
+ align(right, pad(right: 15%, text(fill: gray.darken(20%), size: 0.9em, [--- #cite-content])))
+ }
+ ),
+
+ // Boîte de définitions
+ if definitions != none {
+ context {
+ let conf = config-state.get()
+ block(
+ width: 85%,
+ fill: conf.primary-color.lighten(95%),
+ stroke: (left: 3pt + conf.primary-color), // Bordure gauche élégante
+ inset: 1.5em,
+ radius: (right: 4pt),
+ align(left, {
+ set par(leading: 0.8em)
+ definitions
+ })
+ )
+ }
+ }
+ )
+ })
+}
+
+#let ending-slide(
+ title: [Thanks for watching!],
+ subtitle: [Questions?],
+ contact: ("email@example.com", "github.com/username")
+) = context {
+ let conf = config-state.get()
+ empty-slide(fill: conf.primary-color, {
+ place(top + left, pad(top: 2em, left: 2em, image(conf.logo-transition, width: 5em)))
+ set text(fill: white)
+ align(center + horizon, stack(
+ spacing: 1.5em,
+ text(size: 2.5em, weight: "bold", title),
+ if subtitle != none { text(size: 1.5em, style: "italic", fill: white.transparentize(20%), subtitle) },
+ if contact != none and contact != () {
+ v(1em)
+ set text(size: 1em, weight: "regular")
+ if type(contact) == array {
+ contact.join([ #h(2em) ])
+ } else {
+ contact
+ }
+ }
+ ))
+ })
+}
+
+// --- Boîtes et Blocs ---
+
+#let _base-box(title: none, body, color: black, fill-mode: "outline") = {
+ let (fill-body, stroke-box) = if fill-mode == "fill" {
+ (color.lighten(90%), 0.5pt + color)
+ } else if fill-mode == "full" {
+ (color.lighten(80%), 0.5pt + color)
+ } else if fill-mode == "transparent" {
+ (none, none)
+ } else {
+ // outline
+ (none, 0.5pt + color)
+ }
+
+ block(
+ width: 100%,
+ radius: 4pt,
+ clip: true,
+ stroke: stroke-box,
+ stack(
+ spacing: 0pt,
+ if title != none {
+ block(
+ width: 100%,
+ fill: color,
+ inset: 0.6em,
+ text(fill: white, weight: "bold", title)
+ )
+ },
+ block(
+ width: 100%,
+ fill: fill-body,
+ inset: 0.8em,
+ body
+ )
+ )
+ )
+}
+
+#let highlight-box(title: "Key Point", fill-mode: "outline", body) = {
+ _base-box(title: title, body, color: sorbonne-blue, fill-mode: fill-mode)
+}
+
+#let alert-box(title: "Warning", fill-mode: "outline", body) = {
+ _base-box(title: title, body, color: sorbonne-red, fill-mode: fill-mode)
+}
+
+#let example-box(title: "Example", fill-mode: "outline", body) = {
+ _base-box(title: title, body, color: rgb("#2E7D32"), fill-mode: fill-mode)
+}
+
+#let algorithm-box(title: "Algorithm", fill-mode: "outline", body) = {
+ let algorithm-body = {
+ set text(font: ("Fira Code", "DejaVu Sans Mono"), size: 0.9em)
+ // On formate les listes numérotées comme des lignes de code
+ show enum: it => {
+ grid(
+ columns: (1.5em, 1fr),
+ column-gutter: 0.8em,
+ row-gutter: 0.5em,
+ ..it.children.enumerate().map(((i, child)) => (
+ align(right, text(fill: gray, str(i + 1) + ":")),
+ child.body
+ )).flatten()
+ )
+ }
+ body
+ }
+ _base-box(title: title, algorithm-body, color: rgb("#455A64"), fill-mode: fill-mode)
+}
+
+#let themed-block(title: none, fill-mode: "outline", body) = context {
+ let conf = config-state.get()
+ let color = conf.primary-color
+ _base-box(title: title, body, color: color, fill-mode: fill-mode)
+}
+
+#let appendix() = {
+ counter(heading).update(0)
+ [#metadata(none) ]
+ context {
+ let conf = config-state.get()
+ focus-slide(upper(conf.annex-main-title))
+ }
+}
+
+#let slide-break() = colbreak(weak: true)
+
+// --- Template ---
+
+#let template(
+ title: none,
+ author: none,
+ short-title: none,
+ short-author: none,
+ affiliation: none,
+ subtitle: none,
+ date: datetime.today().display(),
+ aspect-ratio: "16-9",
+ text-font: "Fira Sans",
+ text-size: 20pt,
+ faculty: "sante",
+ // Surcharges optionnelles
+ primary-color: none,
+ alert-color: none,
+ logo-slide: none,
+ logo-transition: none,
+ show-header-numbering: true,
+ numbering-format: "1.1",
+ part-numbering-format: "I",
+ annex-title: [Annexe],
+ annex-main-title: [Annexes],
+ annex-numbering-format: "I",
+ mapping: (section: 1, subsection: 2),
+ bib-style: "apa",
+ transitions: (:),
+ show-outline: false,
+ outline-title: [Sommaire],
+ outline-depth: 2,
+ outline-columns: 1,
+ auto-title: true,
+ progress-bar: "none", // "none", "top", "bottom"
+ slide-break-suffix: [ (cont.)],
+ footer-author: true,
+ footer-title: true,
+ max-length: none,
+ use-short-title: false,
+ body
+) = {
+ // 1. Détermination des valeurs par défaut basées sur faculty
+ let (def-primary, def-alert, def-logo-transition, def-logo-slide) = if faculty == "sciences" {
+ (sorbonne-lightblue, sorbonne-lightblue.darken(40%), "assets/logo/logo-sciences-white.svg", "assets/logo/logo-sciences.svg")
+ } else if faculty == "lettres" {
+ (sorbonne-yellow, sorbonne-yellow.darken(45%), "assets/logo/logo-lettres-white.svg", "assets/logo/logo-lettres.svg")
+ } else if faculty == "univ" or faculty == none {
+ (sorbonne-blue, sorbonne-blue.darken(20%), "assets/logo/logo-univ-white.svg", "assets/logo/logo-univ.svg")
+ } else {
+ // Default is sante
+ (sorbonne-red, sorbonne-red.darken(15%), "assets/logo/logo-sante-white.svg", "assets/logo/logo-sante.svg")
+ }
+
+ // 2. Application des surcharges si fournies
+ let final-primary = if primary-color != none { primary-color } else { def-primary }
+ let final-alert = if alert-color != none { alert-color } else { def-alert }
+ let final-logo-transition = if logo-transition != none { logo-transition } else { def-logo-transition }
+ let final-logo-slide = if logo-slide != none { logo-slide } else { def-logo-slide }
+
+ config-state.update(c => (
+ title: title,
+ author: author,
+ short-title: if short-title != none { short-title } else { title },
+ short-author: if short-author != none { short-author } else { author },
+ affiliation: affiliation,
+ show-header-numbering: show-header-numbering,
+ numbering-format: numbering-format,
+ part-numbering-format: part-numbering-format,
+ annex-title: annex-title,
+ annex-main-title: annex-main-title,
+ annex-numbering-format: annex-numbering-format,
+ mapping: mapping,
+ primary-color: final-primary,
+ alert-color: final-alert,
+ logo-transition: final-logo-transition,
+ logo-slide: final-logo-slide,
+ text-font: text-font,
+ text-size: text-size,
+ progress-bar: progress-bar,
+ slide-break-suffix: slide-break-suffix,
+ footer-author: footer-author,
+ footer-title: footer-title,
+ max-length: max-length,
+ use-short-title: use-short-title,
+ ))
+
+ nav.navigator-config.update(c => {
+ c.mapping = mapping
+ c.auto-title = auto-title
+ c.show-heading-numbering = show-header-numbering
+ c.slide-func = empty-slide
+ c.theme-colors = (primary: final-primary)
+ c.max-length = max-length
+ c.use-short-title = use-short-title
+ c.transitions = (
+ parts: (visibility: (part: "none", section: "none", subsection: "none")),
+ sections: (visibility: (part: "none", section: "none", subsection: "current-parent")),
+ subsections: (visibility: (part: "none", section: "none", subsection: "current-parent")),
+ style: (active-weight: "bold", active-color: white, inactive-opacity: 0.6, completed-opacity: 0.6),
+ marker: none,
+ ) + transitions
+ c.progressive-outline = nav.merge-dicts(
+ (
+ level-1-mode: "none",
+ level-2-mode: "none",
+ level-3-mode: "none",
+ text-styles: (
+ level-1: (active: (weight: "bold", fill: final-primary), completed: (weight: "bold"), inactive: (weight: "bold")),
+ level-2: (active: (weight: "regular", fill: final-primary), completed: (weight: "regular"), inactive: (weight: "regular")),
+ level-3: (active: (weight: "regular", fill: final-primary), completed: (weight: "regular"), inactive: (weight: "regular"))
+ ),
+ ),
+ base: c.at("progressive-outline", default: (:))
+ )
+ c
+ })
+
+ set page(
+ paper: "presentation-" + aspect-ratio,
+ margin: (top: 4.5em, bottom: 3.0em, x: 0pt),
+ header: none,
+ footer: none,
+ foreground: context {
+ let conf = config-state.get()
+ if conf == none { return none }
+
+ // Header & Footer
+ place(top + left, sorbonne-header())
+ place(bottom + left, sorbonne-footer())
+
+ // Progress bar
+ if conf.progress-bar != "none" {
+ let line = progress-bar-line()
+ if conf.progress-bar == "top" {
+ place(top + left, line)
+ } else if conf.progress-bar == "bottom" {
+ place(bottom + left, line)
+ }
+ }
+ }
+ )
+ set text(font: text-font, size: text-size, fill: sorbonne-text)
+ show math.equation: set text(font: "Fira Math")
+
+ // Listes à puces et énumérations thématiques
+ set list(marker: ([•], [‣], [–]).map(m => text(fill: final-primary, m)))
+ set enum(numbering: (n) => text(fill: final-primary, weight: "bold", str(n) + "."))
+
+ // Définit le style de bibliographie
+ set bibliography(style: bib-style)
+
+ // Style des citations
+ show cite: it => context {
+ let conf = config-state.get()
+ box(
+ inset: (x: 2pt),
+ outset: (y: 2pt),
+ radius: 2pt,
+ fill: conf.primary-color.lighten(90%),
+ text(fill: conf.primary-color, it)
+ )
+ }
+
+ set heading(numbering: (..nums) => context {
+
+ if not show-header-numbering { return none }
+ let n = nums.pos()
+
+ // Check if we are in appendix
+ let appendix-marker = query()
+ let is-annex = if appendix-marker.len() > 0 {
+ appendix-marker.first().location().page() < here().page() or (appendix-marker.first().location().page() == here().page() and appendix-marker.first().location().position().y < here().position().y)
+ } else { false }
+
+ if is-annex {
+ let formats = (annex-numbering-format, "A", "1")
+ let parts = ()
+ for i in range(n.len()) {
+ parts.push(numbering(formats.at(i, default: "1"), n.at(i)))
+ }
+ return annex-title + " " + parts.join("")
+ }
+
+ let role = none
+ for (r, lvl) in mapping { if lvl == n.len() { role = r; break } }
+
+ if role == "part" {
+ numbering(part-numbering-format, ..n)
+ } else if role == "section" or role == "subsection" {
+ let start-idx = if mapping.keys().contains("part") { 1 } else { 0 }
+ if n.len() > start-idx {
+ numbering(numbering-format, ..n.slice(start-idx))
+ }
+ } else {
+ none
+ }
+ })
+
+ // Page de Titre
+ empty-slide(fill: final-primary, {
+ set text(fill: white)
+ place(bottom + right, pad(bottom: 2em, right: 2em, image(final-logo-transition, width: 6em)))
+ align(horizon, pad(x: 3em, y: 2em, stack(
+ spacing: 1.2em,
+ text(size: 2.5em, weight: "bold", smallcaps(title)),
+ if subtitle != none { text(size: 1.4em, style: "italic", subtitle) },
+ v(1.5em),
+ text(size: 1.2em, weight: "bold", author),
+ text(size: 1em, affiliation),
+ text(size: 0.9em, fill: white.transparentize(20%), date),
+ )))
+ })
+
+ // Sommaire automatique
+ if show-outline {
+ slide(title: outline-title, {
+ if outline-columns > 1 {
+ columns(outline-columns, outline(title: none, depth: outline-depth, indent: 2em))
+ } else {
+ outline(title: none, depth: outline-depth, indent: 2em)
+ }
+ })
+ }
+
+ show heading: h => context {
+ if h.level > 3 { return h }
+
+ let conf = config-state.get()
+
+ // Check if we are in appendix for this heading
+ let appendix-marker = query()
+ let is-annex = if appendix-marker.len() > 0 {
+ appendix-marker.first().location().page() < h.location().page() or (appendix-marker.first().location().page() == h.location().page() and appendix-marker.first().location().position().y < h.location().position().y)
+ } else { false }
+
+ // En annexe, on ne fait des transitions que pour le niveau le plus haut mappé
+ let top-level = calc.min(..mapping.values())
+ if is-annex and h.level > top-level {
+ return place(hide(h))
+ }
+
+ nav.render-transition(
+ h,
+ top-padding: 0pt,
+ use-short-title: false,
+ content-wrapper: (roadmap, h, active) => {
+ set text(fill: white, font: "Fira Sans")
+ place(top + left, pad(top: 2em, left: 2em, image(conf.logo-transition, width: 5em)))
+
+ let role = none
+ for (r, lvl) in mapping { if lvl == h.level { role = r; break } }
+
+ // --- CASE 1: PART TRANSITION (Centered Title, No Roadmap) ---
+ if role == "part" or (is-annex and role == "section" and not mapping.keys().contains("part")) {
+ align(center + horizon, stack(
+ spacing: 1.5em,
+ if conf.show-header-numbering {
+ let num = if is-annex {
+ conf.annex-title + " " + numbering(conf.annex-numbering-format, counter(heading).at(h.location()).at(0))
+ } else {
+ numbering(conf.part-numbering-format, counter(heading).at(h.location()).at(0))
+ }
+ text(size: if is-annex { 4em } else { 6em }, weight: "bold", num)
+ },
+ text(size: 3em, weight: "bold", upper(h.body))
+ ))
+
+ // --- CASE 2: SECTION TRANSITION (Split Layout with Roadmap) ---
+ } else {
+ // 2.1. Active Part display (Top Right)
+ let part-lvl = mapping.at("part", default: none)
+ let active-part = if part-lvl != none { active.at("h" + str(part-lvl), default: none) } else { none }
+ if active-part != none {
+ place(top + right, pad(top: 2.5em, right: 3em, text(size: 0.8em, fill: white.transparentize(30%), weight: "bold", upper(active-part.body))))
+ }
+
+ // 2.2. Main Content
+ let section-lvl = mapping.at("section", default: 1)
+ let section-head = active.at("h" + str(section-lvl), default: h)
+ let count = counter(heading).at(section-head.location())
+ let start-idx = if mapping.keys().contains("part") { 1 } else { 0 }
+ let nums = count.slice(start-idx)
+
+ pad(x: 2em, stack(
+ dir: ttb,
+ v(15%),
+ align(center, stack(
+ spacing: 0.8em,
+ if conf.show-header-numbering {
+ let fmt-num = if is-annex {
+ conf.annex-title + " " + numbering(conf.annex-numbering-format, ..nums)
+ } else {
+ numbering(conf.numbering-format, ..nums)
+ }
+ if is-annex {
+ text(size: 3.5em, weight: "bold", fmt-num + " " + smallcaps(section-head.body))
+ } else {
+ text(size: 6em, weight: "bold", fmt-num)
+ }
+ },
+ if not is-annex { text(size: 2.2em, weight: "bold", smallcaps(section-head.body)) },
+ v(1.2em),
+ block(width: 60%, align(left, roadmap))
+ ))
+ ))
+ }
+ }
+ )
+ }
+
+ body
+}
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/template/main.typ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/template/main.typ
new file mode 100644
index 0000000000..fb9de6f9d0
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/template/main.typ
@@ -0,0 +1,46 @@
+#import "@preview/unofficial-sorbonne-presentation:0.1.0": *
+
+// --- Theme Configuration ---
+#show: template.with(
+ title: [Presentation Title],
+ subtitle: [Subtitle or Context],
+ author: [Your Name],
+ affiliation: [Your Laboratory / Department],
+ // faculty: "univ", // Presets: "univ" (blue), "sante" (red), "sciences" (light blue), "lettres" (yellow)
+ date: datetime.today().display(),
+ show-outline: true, // Show the table of contents at the beginning
+)
+
+// --- Content ---
+
+= Introduction
+
+#slide(title: "Welcome")[
+ This is a sample presentation using the Unofficial Sorbonne theme.
+
+ - Respects the university's visual identity.
+ - Built on top of `presentate` and `navigator` packages.
+]
+
+= First Part
+
+== Key Concepts
+
+#slide[
+ You can use numbered and bulleted lists:
+ + First important point
+ + Second crucial point
+ - Technical detail
+]
+
+#focus-slide[
+ "Focus" slides are designed for impactful messages or major transitions.
+]
+
+= Conclusion
+
+#ending-slide(
+ title: [Thank you for your attention!],
+ subtitle: [Any questions?],
+ contact: ("first.name@my-universite.fr",)
+)
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/thumbnail.png b/packages/preview/unofficial-sorbonne-presentation/0.1.0/thumbnail.png
new file mode 100644
index 0000000000..b1f8f4a005
Binary files /dev/null and b/packages/preview/unofficial-sorbonne-presentation/0.1.0/thumbnail.png differ
diff --git a/packages/preview/unofficial-sorbonne-presentation/0.1.0/typst.toml b/packages/preview/unofficial-sorbonne-presentation/0.1.0/typst.toml
new file mode 100644
index 0000000000..d2bdbf19a7
--- /dev/null
+++ b/packages/preview/unofficial-sorbonne-presentation/0.1.0/typst.toml
@@ -0,0 +1,16 @@
+[package]
+name = "unofficial-sorbonne-presentation"
+version = "0.1.0"
+entrypoint = "lib.typ"
+authors = ["David Hajage"]
+license = "MIT"
+keywords = ["presentation", "sorbonne", "academic", "slides"]
+description = "A non-official structured presentation theme designed to match the branding and style guidelines of the Sorbonne University, based on presentate and navigator."
+compiler = "0.12.0"
+categories = ["presentation"]
+exclude = ["tests", "*.pdf", "**/*.pdf", ".gitignore", ".DS_Store"]
+
+[template]
+path = "template"
+entrypoint = "main.typ"
+thumbnail = "thumbnail.png"