feature/app-theming-and-code-slides #2

Merged
brenno merged 4 commits from feature/app-theming-and-code-slides into main 2026-06-08 12:31:04 +00:00
12 changed files with 312 additions and 59 deletions
Showing only changes of commit e0379ade59 - Show all commits

View file

@ -9,9 +9,11 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
### Added
- **Source-code slides** — a "code sheet" with per-language syntax highlighting,
stored as a fenced code block. Background and text colours are part of the style
profile, with a syntax-colouring toggle; turning it off renders the block in a
single colour (e.g. green on black for a CRT-terminal look).
stored as a fenced code block. Background, text colour and monospace font are
part of the style profile, with a syntax-colouring toggle; turning it off renders
the block in a single colour (e.g. green on black for a CRT-terminal look). The
code is sized to fill the panel — larger when there's room, smaller for long
fragments.
- **Charts** — bar, line, pie, and **spider/radar** chart slides. Data is entered
in an in-app grid or imported from CSV; the spec is stored as JSON in a ```chart
block. Data can stay inline or be linked to a CSV in a separate `data/`

View file

@ -9,7 +9,7 @@ Built with Flutter for macOS, Windows, and Linux.
## Features
- **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, source code, charts, and free-form Markdown.
- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background and text colours come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look).
- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background, text colour and monospace font come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look). The code auto-sizes to fill the panel.
- **Charts** — bar, line, pie, and spider/radar charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory. Optional min/max draw reference lines (bar/line) or fix the scale (radar); legend hover highlights a series, and line tooltips pick the dot under the cursor.
- **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math.
- **Traffic Light Protocol** — a deck-wide classification plus an optional **per-slide TLP level**; slides classified stricter than the level the deck is shown at are automatically withheld, both when presenting and exporting.

View file

@ -138,6 +138,7 @@ JSON heeft deze velden (met standaardwaarden):
| `codeBackgroundColor` | `#282C34` | Achtergrond van broncode-slides. |
| `codeTextColor` | `#ABB2BF` | Tekstkleur van broncode-slides. |
| `codeHighlightSyntax` | `true` | Syntaxkleuring aan/uit. Uit = alles in één kleur (bijv. groen op zwart voor een CRT-look). |
| `codeFontFamily` | `monospace` | Lettertype van broncode-slides (bijv. `Courier New`). |
| `logoPath` | `null` | Pad naar logo (relatief in `logos/`). |
| `logoPosition` | `bottom-right` | `top-left`/`top-right`/`bottom-left`/`bottom-right`. |
| `logoSize` | `96` | Logogrootte in px. |

View file

@ -31,10 +31,12 @@ highlighting and `$…$` / `$$…$$` LaTeX math.
### Source-code slides
Choose a programming language for syntax highlighting (or "plain text") and paste
your code. It renders as a "code sheet" whose background and text colour come from
the active **style profile**. Turn **syntax colouring** off to show the whole block
in a single colour — e.g. bright green on black for a classic CRT-terminal look.
Stored as a fenced code block in the Markdown.
your code. It renders as a "code sheet" whose background, text colour and
**monospace font** come from the active **style profile** (e.g. Courier). Turn
**syntax colouring** off to show the whole block in a single colour — e.g. bright
green on black for a classic CRT-terminal look. The text is sized to fill the
panel — larger when there's room, smaller for long fragments. Stored as a fenced
code block in the Markdown.
### Charts
@ -112,10 +114,11 @@ Export to:
## Theming and language
- **Style profiles** control deck colours (including the source-code background and
text, with an optional syntax-colouring toggle), fonts, logo, and footer. Every
colour can be picked from the presets or entered as a custom hex value. The
bundled Marp theme is `assets/themes/ocideck.css`.
- **Style profiles** control deck colours (including the source-code background,
text, font and an optional syntax-colouring toggle), fonts, logo, and footer.
Every colour can be picked from the presets or entered as a custom hex value. The
Colours and Logo tabs show which profile you're editing. The bundled Marp theme
is `assets/themes/ocideck.css`.
- **App appearance** (including a dark interface) is configurable in settings.
- The interface is available in Dutch, English, Italian, German, French, Spanish,
Frisian, and Papiamento.

View file

@ -2382,6 +2382,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Custom colour (hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'For example #33FF33 for a CRT-green screen.',
'Onderdeel van stijlprofiel ': 'Part of style profile ',
'Broncode lettertype': 'Code font',
'Systeem (monospace)': 'System (monospace)',
'Platte tekst': 'Plain text',
'Titel (optioneel)': 'Title (optional)',
'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.':
@ -2418,6 +2421,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Colore personalizzato (hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'Ad esempio #33FF33 per uno schermo verde CRT.',
'Onderdeel van stijlprofiel ': 'Parte del profilo di stile ',
'Broncode lettertype': 'Font del codice',
'Systeem (monospace)': 'Sistema (monospace)',
'Kleur van reeks': 'Colore della serie',
'Kleur van rij': 'Colore della riga',
'Hexkleur': 'Colore esadecimale',
@ -2644,6 +2650,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Eigene Farbe (Hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'Zum Beispiel #33FF33 für einen CRT-grünen Bildschirm.',
'Onderdeel van stijlprofiel ': 'Teil des Stilprofils ',
'Broncode lettertype': 'Code-Schriftart',
'Systeem (monospace)': 'System (monospace)',
'Kleur van reeks': 'Reihenfarbe',
'Kleur van rij': 'Zeilenfarbe',
'Hexkleur': 'Hex-Farbe',
@ -2871,6 +2880,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Couleur personnalisée (hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'Par exemple #33FF33 pour un écran vert CRT.',
'Onderdeel van stijlprofiel ': 'Fait partie du profil de style ',
'Broncode lettertype': 'Police du code',
'Systeem (monospace)': 'Système (monospace)',
'Kleur van reeks': 'Couleur de la série',
'Kleur van rij': 'Couleur de la ligne',
'Hexkleur': 'Couleur hexadécimale',
@ -3098,6 +3110,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Color personalizado (hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'Por ejemplo #33FF33 para una pantalla verde CRT.',
'Onderdeel van stijlprofiel ': 'Parte del perfil de estilo ',
'Broncode lettertype': 'Fuente del código',
'Systeem (monospace)': 'Sistema (monospace)',
'Kleur van reeks': 'Color de la serie',
'Kleur van rij': 'Color de la fila',
'Hexkleur': 'Color hexadecimal',
@ -3325,6 +3340,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Eigen kleur (hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'Bygelyks #33FF33 foar in CRT-grien skerm.',
'Onderdeel van stijlprofiel ': 'Underdiel fan stylprofyl ',
'Broncode lettertype': 'Boarnekoade lettertype',
'Systeem (monospace)': 'Systeem (monospace)',
'Kleur van reeks': 'Rigekleur',
'Kleur van rij': 'Rijekleur',
'Hexkleur': 'Hekskleur',
@ -3549,6 +3567,9 @@ const _dutchSourceStringAdditions = {
'Eigen kleur (hex)': 'Koló propio (hex)',
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
'Por ehèmpel #33FF33 pa un pantaya berde CRT.',
'Onderdeel van stijlprofiel ': 'Parti di e perfil di estilo ',
'Broncode lettertype': 'Tipo di lèter di kódigo',
'Systeem (monospace)': 'Sistema (monospace)',
'Kleur van reeks': 'Koló di serie',
'Kleur van rij': 'Koló di liña',
'Hexkleur': 'Koló hexadecimal',

View file

@ -19,6 +19,10 @@ class ThemeProfile {
/// syntax colours) required for a believable single-colour CRT screen.
final bool codeHighlightSyntax;
/// Monospace font family for code slides. `monospace` uses the system default;
/// e.g. `Courier New` for a typewriter look.
final String codeFontFamily;
final String? logoPath;
final String logoPosition;
final int logoSize;
@ -54,6 +58,7 @@ class ThemeProfile {
this.codeBackgroundColor = '#282C34',
this.codeTextColor = '#ABB2BF',
this.codeHighlightSyntax = true,
this.codeFontFamily = 'monospace',
this.logoPath,
this.logoPosition = 'bottom-right',
this.logoSize = 96,
@ -87,6 +92,7 @@ class ThemeProfile {
String? codeBackgroundColor,
String? codeTextColor,
bool? codeHighlightSyntax,
String? codeFontFamily,
String? logoPath,
String? logoPosition,
int? logoSize,
@ -112,6 +118,7 @@ class ThemeProfile {
codeBackgroundColor: codeBackgroundColor ?? this.codeBackgroundColor,
codeTextColor: codeTextColor ?? this.codeTextColor,
codeHighlightSyntax: codeHighlightSyntax ?? this.codeHighlightSyntax,
codeFontFamily: codeFontFamily ?? this.codeFontFamily,
logoPath: clearLogo ? null : (logoPath ?? this.logoPath),
logoPosition: logoPosition ?? this.logoPosition,
logoSize: logoSize ?? this.logoSize,
@ -139,6 +146,7 @@ class ThemeProfile {
'codeBackgroundColor': codeBackgroundColor,
'codeTextColor': codeTextColor,
'codeHighlightSyntax': codeHighlightSyntax,
'codeFontFamily': codeFontFamily,
'logoPath': logoPath,
'logoPosition': logoPosition,
'logoSize': logoSize,
@ -173,6 +181,7 @@ class ThemeProfile {
json['codeBackgroundColor'] as String? ?? '#282C34',
codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF',
codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true,
codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace',
logoPath: json['logoPath'] as String?,
logoPosition: json['logoPosition'] as String? ?? 'bottom-right',
logoSize: (json['logoSize'] as num?)?.round() ?? 96,
@ -370,6 +379,17 @@ class AppSettings {
'Courier New',
];
/// Monospace families offered for code slides. `monospace` is the system
/// default; the rest are common typewriter/coding faces.
static const codeFonts = [
'monospace',
'Courier New',
'Menlo',
'Consolas',
'Roboto Mono',
'Cascadia Code',
];
AppSettings copyWith({
String? languageCode,
String? homeDirectory,

View file

@ -589,6 +589,11 @@ class MarpHtmlService {
Future<String> _themedCss(ThemeProfile t) async {
final fontFace = await _ebGaramondFontFace(t.fontFamily);
final family = _cssFontStack(t.fontFamily);
final codePrefix = t.codeFontFamily == 'monospace'
? ''
: "'${t.codeFontFamily}',";
final codeFamily =
'${codePrefix}SFMono-Regular,Consolas,"Liberation Mono",monospace';
return '$fontFace\n'
'*{box-sizing:border-box}'
'html,body{margin:0;padding:0}'
@ -603,9 +608,9 @@ class MarpHtmlService {
'.slide p,.slide li{font-size:24px;line-height:1.45}'
'.slide pre{background:${t.codeBackgroundColor};color:${t.codeTextColor};'
'border:1px solid ${t.codeTextColor}38;border-radius:6px;'
'padding:16px;overflow:auto;font-size:18px}'
'padding:16px;overflow:auto;font-size:18px;font-family:$codeFamily}'
'.slide pre code{color:${t.codeTextColor};background:transparent}'
'.slide code{font-family:SFMono-Regular,Consolas,"Liberation Mono",monospace}'
'.slide code{font-family:$codeFamily}'
'.slide pre.mermaid{background:transparent;border:0;text-align:center}'
'.slide img{max-width:100%}'
'.slide blockquote{border-left:4px solid ${t.accentColor};margin:.5em 0;'

View file

@ -882,11 +882,47 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
);
}
/// A banner shown on tabs that edit the active style profile, so it is clear
/// these settings belong to the loaded profile (and which one).
Widget _profileScopeBanner() {
final name = _themeProfile.name;
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.accent.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border(left: BorderSide(color: AppTheme.accent, width: 3)),
),
child: Row(
children: [
Icon(Icons.style_outlined, size: 16, color: AppTheme.accent),
const SizedBox(width: 8),
Expanded(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: context.l10n.d('Onderdeel van stijlprofiel ')),
TextSpan(
text: '$name',
style: const TextStyle(fontWeight: FontWeight.w700),
),
],
),
style: const TextStyle(fontSize: 12, color: Color(0xFF334155)),
),
),
],
),
);
}
Widget _colorsTab() {
final l10n = context.l10n;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_profileScopeBanner(),
_sectionTitle(l10n.d('Kleuren')),
_colorSetting(
l10n.d('Achtergrond slides'),
@ -974,6 +1010,33 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
contentPadding: EdgeInsets.zero,
dense: true,
),
const SizedBox(height: 10),
DropdownButtonFormField<String>(
initialValue: AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
? _themeProfile.codeFontFamily
: 'monospace',
decoration: InputDecoration(
labelText: l10n.d('Broncode lettertype'),
isDense: true,
),
items: [
for (final f in AppSettings.codeFonts)
DropdownMenuItem(
value: f,
child: Text(
f == 'monospace' ? l10n.d('Systeem (monospace)') : f,
style: TextStyle(fontFamily: f),
),
),
],
onChanged: (v) {
if (v == null) return;
setState(() {
_themeProfile = _themeProfile.copyWith(codeFontFamily: v);
_profileTouched = true;
});
},
),
const SizedBox(height: 18),
_stylePreview(),
],
@ -985,6 +1048,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_profileScopeBanner(),
_sectionTitle(l10n.d('Logo')),
Row(
children: [

View file

@ -2086,6 +2086,16 @@ class _CodePreview extends StatelessWidget {
required this.profile,
});
/// Natural (unwrapped) size of [text] in [style]: width is the longest line,
/// height the full block. Used to scale code to the available space.
static Size _measureMono(String text, TextStyle style) {
final painter = TextPainter(
text: TextSpan(text: text.isEmpty ? ' ' : text, style: style),
textDirection: TextDirection.ltr,
)..layout();
return painter.size;
}
@override
Widget build(BuildContext context) {
_ensureHighlightLanguages();
@ -2098,10 +2108,20 @@ class _CodePreview extends StatelessWidget {
final codeBg = _hexColor(profile.codeBackgroundColor);
final codeFg = _hexColor(profile.codeTextColor);
final mono = TextStyle(
fontFamily: 'monospace',
fontFamilyFallback: const ['Menlo', 'Consolas', 'Courier New'],
fontSize: w * 0.024,
// The chosen monospace family, always backed by a generic monospace fallback
// so an uninstalled face still renders fixed-width.
final fallback = <String>[
'Menlo',
'Consolas',
'Courier New',
'monospace',
]..removeWhere((f) => f == profile.codeFontFamily);
final baseFont = w * 0.024;
final maxFont = w * 0.040; // grow to fill, but never huge
TextStyle monoAt(double size) => TextStyle(
fontFamily: profile.codeFontFamily,
fontFamilyFallback: fallback,
fontSize: size,
height: 1.4,
color: codeFg,
);
@ -2109,23 +2129,25 @@ class _CodePreview extends StatelessWidget {
// HighlightView throws on an unknown language, so fall back to plain (but
// monospace) text. When syntax highlighting is off we always render plain
// text so the whole block is one colour needed for a CRT-green screen.
final Widget codeContent = (known && profile.codeHighlightSyntax)
final useHighlight = known && profile.codeHighlightSyntax;
final highlightTheme = {
...atomOneDarkTheme,
// Keep atom-one-dark's per-token colours but drop its own background so
// our themed [codeBg] shows through unchanged.
'root': (atomOneDarkTheme['root'] ?? const TextStyle()).copyWith(
backgroundColor: codeBg,
color: codeFg,
),
};
Widget buildCode(TextStyle style) => useHighlight
? HighlightView(
code,
language: lang,
// Keep atom-one-dark's per-token colours but drop its own
// background so our themed [codeBg] shows through unchanged.
theme: {
...atomOneDarkTheme,
'root': (atomOneDarkTheme['root'] ?? const TextStyle()).copyWith(
backgroundColor: codeBg,
color: codeFg,
),
},
theme: highlightTheme,
padding: EdgeInsets.zero,
textStyle: mono,
textStyle: style,
)
: Text(code, style: mono);
: Text(code, style: style);
return Container(
color: _hexColor(profile.slideBackgroundColor),
@ -2136,45 +2158,80 @@ class _CodePreview extends StatelessWidget {
pad,
pad + safe.bottom,
),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: codeBg,
borderRadius: BorderRadius.circular(w * 0.012),
border: Border.all(color: codeFg.withValues(alpha: 0.22)),
),
padding: EdgeInsets.all(w * 0.03),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (slide.title.isNotEmpty) ...[
_md(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// The slide title belongs to the slide, not inside the code window,
// so it sits above the panel like other slide types.
if (slide.title.isNotEmpty) ...[
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: w * 0.025,
vertical: w * 0.01,
),
decoration: BoxDecoration(
color: _hexColor(profile.titleBackgroundColor),
borderRadius: BorderRadius.circular(w * 0.012),
border: Border(
left: BorderSide(
color: _hexColor(profile.accentColor),
width: w * 0.006,
),
),
),
child: _md(
context,
slide.title,
_applyFont(
font,
TextStyle(
fontSize: w * 0.03,
fontSize: w * 0.032,
height: 1.1,
fontWeight: FontWeight.bold,
color: codeFg,
color: _hexColor(profile.titleTextColor),
),
),
linkColor: _hexColor(profile.accentColor),
),
SizedBox(height: w * 0.02),
],
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.topLeft,
// Een onbegrensde breedte laat code-regels op hun natuurlijke
// lengte staan (geen woordafbreking), waarna de FittedBox het
// geheel verkleint tot het past.
child: codeContent,
),
SizedBox(height: w * 0.018),
],
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: codeBg,
borderRadius: BorderRadius.circular(w * 0.012),
border: Border.all(color: codeFg.withValues(alpha: 0.22)),
),
padding: EdgeInsets.all(w * 0.03),
child: LayoutBuilder(
builder: (context, constraints) {
// Size the code to fill the panel: scale up to use spare
// space (capped at [maxFont]) and down so long fragments
// still fit, rather than leaving a small block in a big box.
final measured = useHighlight
? code.replaceAll('\t', ' ')
: code;
final natural = _measureMono(measured, monoAt(baseFont));
final availW = math.max(1.0, constraints.maxWidth - 1);
final availH = math.max(1.0, constraints.maxHeight - 1);
var scale = math.min(
availW / natural.width,
availH / natural.height,
);
if (!scale.isFinite || scale <= 0) scale = 1;
final size = math.min(baseFont * scale, maxFont);
return Align(
alignment: Alignment.topLeft,
child: buildCode(monoAt(size)),
);
},
),
),
],
),
),
],
),
),
);

View file

@ -80,4 +80,78 @@ void main() {
expect(codeText.style?.color, _hex('#33FF33'));
expect(tester.takeException(), isNull);
});
testWidgets('short code is enlarged to use the space; long code shrinks', (
tester,
) async {
const profile = ThemeProfile(codeHighlightSyntax: false);
const short = 'x';
await tester.pumpWidget(
_host(
Slide.create(SlideType.code).copyWith(customMarkdown: short),
profile,
),
);
await tester.pump();
final shortSize = tester.widget<Text>(find.text(short)).style!.fontSize!;
final long = List.generate(
40,
(i) => 'final someRatherLongVariableName$i = compute($i);',
).join('\n');
await tester.pumpWidget(
_host(
Slide.create(SlideType.code).copyWith(customMarkdown: long),
profile,
),
);
await tester.pump();
final longSize = tester.widget<Text>(find.text(long)).style!.fontSize!;
// A tiny snippet is scaled up to fill; a big one is scaled down to fit.
expect(longSize, lessThan(shortSize));
expect(tester.takeException(), isNull);
});
testWidgets('the slide title sits above the code panel, not inside it', (
tester,
) async {
final slide = Slide.create(
SlideType.code,
).copyWith(title: 'Voorbeeld', customMarkdown: 'print("hi")');
const profile = ThemeProfile(
titleTextColor: '#FFFFFF',
titleBackgroundColor: '#1C2B47',
codeBackgroundColor: '#000000',
codeTextColor: '#33FF33',
codeHighlightSyntax: false,
);
await tester.pumpWidget(_host(slide, profile));
await tester.pump();
// The title is rendered above the code panel rather than inside it.
final titleBottom = tester.getBottomLeft(find.text('Voorbeeld')).dy;
final codeTop = tester.getTopLeft(find.text('print("hi")')).dy;
expect(titleBottom, lessThanOrEqualTo(codeTop));
expect(tester.takeException(), isNull);
});
testWidgets('code uses the chosen monospace font family', (tester) async {
final slide = Slide.create(
SlideType.code,
).copyWith(customMarkdown: 'void main() {}');
const profile = ThemeProfile(
codeFontFamily: 'Courier New',
codeHighlightSyntax: false,
);
await tester.pumpWidget(_host(slide, profile));
await tester.pump();
final codeText = tester.widget<Text>(find.text('void main() {}'));
expect(codeText.style?.fontFamily, 'Courier New');
expect(tester.takeException(), isNull);
});
}

View file

@ -106,11 +106,14 @@ void main() {}
const theme = ThemeProfile(
codeBackgroundColor: '#000000',
codeTextColor: '#33FF33',
codeFontFamily: 'Courier New',
);
final html = await service.build('```dart\nvoid main() {}\n```', theme: theme);
expect(html, contains('.slide pre{background:#000000;color:#33FF33'));
expect(html, contains('.slide pre code{color:#33FF33'));
// The chosen code font is used (with a monospace fallback chain).
expect(html, contains("font-family:'Courier New',"));
});
test('EB Garamond theme embeds the font for offline rendering', () async {

View file

@ -15,24 +15,27 @@ Future<SettingsNotifier> _loadedNotifier() async {
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('ThemeProfile round-trips the code colours through JSON', () {
test('ThemeProfile round-trips the code styling through JSON', () {
const profile = ThemeProfile(
codeBackgroundColor: '#000000',
codeTextColor: '#33FF33',
codeHighlightSyntax: false,
codeFontFamily: 'Courier New',
);
final back = ThemeProfile.fromJson(profile.toJson());
expect(back.codeBackgroundColor, '#000000');
expect(back.codeTextColor, '#33FF33');
expect(back.codeHighlightSyntax, isFalse);
expect(back.codeFontFamily, 'Courier New');
});
test('ThemeProfile code colours default to the atom-one-dark look', () {
test('ThemeProfile code styling defaults to the atom-one-dark look', () {
// Older decks without the fields fall back to the dark editor defaults.
final back = ThemeProfile.fromJson(const {'name': 'Legacy'});
expect(back.codeBackgroundColor, '#282C34');
expect(back.codeTextColor, '#ABB2BF');
expect(back.codeHighlightSyntax, isTrue);
expect(back.codeFontFamily, 'monospace');
});
test('starts with a single default profile', () async {