Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions demo/macos/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
PODS:
- FlutterMacOS (1.0.0)
- irondash_engine_context (0.0.1):
- FlutterMacOS
- super_native_extensions (0.0.1):
- FlutterMacOS

DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)

EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
irondash_engine_context:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
super_native_extensions:
:path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos

SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189

PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3

Expand Down
18 changes: 18 additions & 0 deletions demo/macos/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
8A14AB4F804B7CAA2E64DF2E /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -392,6 +393,23 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
8A14AB4F804B7CAA2E64DF2E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E1EAB9F2FA8DCA9689F733BC /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down
51 changes: 51 additions & 0 deletions docs/reference/deck-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,57 @@ DeckOptions(
)
```

#### Named widget block styles

Use `BlockVariant` in a `SlideStyle` stylesheet to target every named widget
block with an exact, case-sensitive name. The selector applies to the block
container and its complete widget subtree, so container spacing and descendant
Mix styles resolve together.

```dart
import 'package:mix/mix.dart';
import 'package:superdeck/superdeck.dart';

const webviewBlock = BlockVariant('webview');

final options = DeckOptions(
baseStyle: SlideStyle(
blockContainer: BoxStyler(
padding: EdgeInsetsGeometryMix.all(40),
).variants([
VariantStyle(
webviewBlock,
BoxStyler(padding: EdgeInsetsGeometryMix.all(0)),
),
]),
),
);
```

`BlockVariant('webview')` matches both `@webview` and `@widget` blocks whose
`name` is `webview`. Custom shorthand annotations work the same way, so
`@chart` matches `BlockVariant('chart')`. The matching rule uses normal Mix
precedence after the base style, and its margin and padding are included when
SuperDeck calculates the child widget's usable size.

SuperDeck already ships this exact zero padding and margin rule for `@webview`
blocks by default, so a webview renders edge-to-edge without any stylesheet. The
example above therefore doubles as the pattern for giving any other named block
its own spacing.

Only `WidgetBlock` names are selectable. Content blocks and section containers
are not selectable; there are no per-instance IDs, classes, attributes, or
selector composition. A plain `NamedVariant('image')` remains a manual Mix
variant and is not activated merely because an `@image` block is rendered.
Every widget block with the same name receives the same rule. Hardcoded styling
inside a custom widget is unchanged unless that widget declares Mix styles that
can respond to the inherited `BlockVariant`.

Fullscreen layout does not create a separate block selector. It only removes
header and footer chrome, so the same `BlockVariant('webview')` rule applies to
normal and fullscreen WebView blocks; those two layouts cannot receive
different rules based on `BlockVariant` alone.

### `styles`
**Type:** `Map<String, SlideStyle>` | **Default:** `{}`

Expand Down
12 changes: 12 additions & 0 deletions docs/reference/markdown-syntax.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ SuperDeck supports three core block types.

Built-in widgets (`image`, `dartpad`, `webview`, `qrcode`) use the same widget syntax as `@widget`.

### Styling named widget blocks

Widget names can be selected from Dart stylesheets with `BlockVariant`. For
example, `@webview` and the equivalent `@widget { name: "webview" }` both
match `BlockVariant('webview')`; `@chart` matches `BlockVariant('chart')`.
Matching is exact and case-sensitive.

The selector affects the widget block container and descendants in that widget
subtree. It does not select `@block`, `@section`, individual block instances,
or arbitrary `NamedVariant` values. See the [DeckOptions reference](/reference/deck-options#named-widget-block-styles)
for the stylesheet declaration and limitations.

## Common block properties

### `flex`
Expand Down
7 changes: 5 additions & 2 deletions packages/superdeck/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
## Unreleased

- Support `layout: fullscreen` slides, which suppress resolved headers and
footers while preserving the resolved background and style.
- Add `BlockVariant` for opt-in, name-based `WidgetBlock` styling in Dart
stylesheets.
- Render `@webview` blocks edge-to-edge (zero padding and margin) by default.
- Always paint an opaque slide background so background-less slides no longer
bleed adjacent slides through during transitions.
- Export `FileDeckLoader` and `BundledDeckLoader` from the public barrel so
apps can use `SuperDeckApp.deckLoader` without importing from `src/`.
- Fall back to bundled deck assets when auto-selection cannot discover a
Expand Down
24 changes: 16 additions & 8 deletions packages/superdeck/lib/src/rendering/blocks/block_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:mix/mix.dart';
import 'package:superdeck_core/superdeck_core.dart';

import '../../deck/slide_configuration.dart';
import '../../styling/block_variant.dart';
import '../../styling/components/slide.dart';
import '../../ui/widgets/error_widgets.dart';
import '../../ui/widgets/overflow_clip.dart';
Expand Down Expand Up @@ -39,8 +40,12 @@ class _BlockContainer extends StatefulWidget {
class _BlockContainerState extends State<_BlockContainer> {
@override
Widget build(context) {
// Get the resolved SlideSpec (provided by SlideView)
final spec = SlideSpec.of(context);
// Widget blocks resolve their style inside [BlockVariantScope] so a named
// block variant affects both the rendered container and its child size.
final spec = switch (widget.block) {
WidgetBlock() => widget.configuration.style.resolve(context).spec,
ContentBlock() => SlideSpec.of(context),
};

final blockOffset = spec.blockContainer.spec.calculateBlockOffset;

Expand Down Expand Up @@ -204,12 +209,15 @@ class CustomBlockWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
return _BlockContainer(
block: block,
size: size,
configuration: configuration,
runtimeKey: runtimeKey,
child: _CustomBlockChild(block: block),
return BlockVariantScope(
name: block.name,
child: _BlockContainer(
block: block,
size: size,
configuration: configuration,
runtimeKey: runtimeKey,
child: _CustomBlockChild(block: block),
),
);
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/superdeck/lib/src/rendering/slides/slide_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ class SlideView extends StatelessWidget {
size: kResolution,
child: Stack(
children: [
// Opaque floor so a slide is never transparent: a transparent or
// absent background part would otherwise let other slides bleed
// through during cross-fade transitions.
const Positioned.fill(
child: ColoredBox(color: kSlideBackgroundColor),
),
// Background fills entire viewport (not affected by modifier)
Positioned.fill(child: backgroundWidget),
// Content wrapped with StyleBuilder to apply modifiers
Expand Down
57 changes: 57 additions & 0 deletions packages/superdeck/lib/src/styling/block_variant.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/widgets.dart';
import 'package:mix/mix.dart';

/// A Mix context selector for a named widget-block container and subtree.
///
/// A block variant is active only while building a widget block whose resolved
/// name exactly matches [name].
final class BlockVariant extends ContextVariant {
/// The widget block name this variant matches.
final String name;

const BlockVariant(this.name) : super('block_$name', _neverMatches);

@override
bool Function(BuildContext) get shouldApply => when;

@override
bool when(BuildContext context) {
return BlockVariantScope.maybeOf(context)?.name == name;
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is BlockVariant && other.name == name;
}

@override
int get hashCode => name.hashCode;
}

bool _neverMatches(BuildContext _) => false;

/// Provides the current named widget block to descendants.
///
/// This scope is used by SuperDeck's renderer. It is intentionally not
/// exported from the public package barrel.
class BlockVariantScope extends InheritedWidget {
/// The resolved widget-block name for this subtree.
final String name;

const BlockVariantScope({
super.key,
required this.name,
required super.child,
});

/// Returns the nearest block variant scope, if one is active.
static BlockVariantScope? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<BlockVariantScope>();
}

@override
bool updateShouldNotify(BlockVariantScope oldWidget) {
return name != oldWidget.name;
}
}
13 changes: 13 additions & 0 deletions packages/superdeck/lib/src/styling/default_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mix/mix.dart';

import 'block_variant.dart';
import 'components/markdown_alert.dart';
import 'components/markdown_alert_type.dart';
import 'components/markdown_blockquote.dart';
Expand Down Expand Up @@ -30,6 +31,11 @@ TextStyle get _baseTextStyle => _safeGoogleFont(
const onGist = NamedVariant('gist');
const onImage = NamedVariant('image');

// A webview fills its own block, so it ships edge-to-edge by default. This uses
// [BlockVariant] because a plain [NamedVariant] does not activate for a rendered
// widget block; only [BlockVariant] reliably targets the `@webview` container.
const onWebview = BlockVariant('webview');

WidgetModifierConfig _pad(EdgeInsetsGeometryMix value) =>
WidgetModifierConfig.padding(value);

Expand Down Expand Up @@ -230,6 +236,13 @@ SlideStyle _createDefaultSlideStyle() {
margin: EdgeInsetsGeometryMix.all(0),
),
),
VariantStyle(
onWebview,
BoxStyler(
padding: EdgeInsetsGeometryMix.all(0),
margin: EdgeInsetsGeometryMix.all(0),
),
),
]),

slideContainer: BoxStyler(),
Expand Down
2 changes: 1 addition & 1 deletion packages/superdeck/lib/src/ui/app_shell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ class _SplitViewState extends State<SplitView>
final buildFailure = deckController.session.buildFailure.value;

return Scaffold(
backgroundColor: const Color.fromARGB(255, 9, 9, 9),
backgroundColor: kSlideBackgroundColor,
floatingActionButtonLocation:
FloatingActionButtonLocation.miniEndFloat,
floatingActionButton: _buildFloatingAction(
Expand Down
9 changes: 9 additions & 0 deletions packages/superdeck/lib/src/utils/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,14 @@ const _kHeight = 720.0;

const kResolution = Size(_kWidth, _kHeight);

/// Opaque backdrop painted behind a slide when no background part is set.
///
/// Guarantees a slide is never transparent: without it, a background-less slide
/// (e.g. a template with no chrome) would show through to whatever is behind it,
/// which bleeds the outgoing/incoming slide through during cross-fade
/// transitions. Matches the app shell letterbox so the default is invisible
/// except during transitions.
const kSlideBackgroundColor = Color(0xFF090909);

const kIsTest = bool.fromEnvironment('FLUTTER_TEST');
const kCanRunProcess = kDebugMode && !kIsWeb && !kIsTest;
1 change: 1 addition & 0 deletions packages/superdeck/lib/superdeck.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export 'src/thumbnails/async_thumbnail.dart';
export 'src/thumbnails/thumbnail_service.dart';

// Styling
export 'src/styling/block_variant.dart' show BlockVariant;
export 'src/styling/default_style.dart';
export 'src/styling/components/markdown_alert.dart';
export 'src/styling/components/markdown_alert_type.dart';
Expand Down
Loading
Loading