The Garage SDKs, in the open

garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
ImBenji
2026-09-23 18:49:21 +01:00
co-authored by Claude Opus 5.5
commit b269201919
117 changed files with 26944 additions and 0 deletions
+308
View File
@@ -0,0 +1,308 @@
import "package:flutter/widgets.dart";
import "panel.dart";
import "theme/garage_theme.dart";
/// The reusable desktop editor frame.
///
/// Applications provide the content for each slot. The frame owns the
/// geometry and visual separation between the header, workspace, docks, and
/// footer, so an editor can keep its app-specific controls outside GarageUI.
class EditorShell extends StatelessWidget {
const EditorShell({
super.key,
required this.center,
this.header,
this.footer,
this.left,
this.right,
this.leftWidth = 0,
this.rightWidth = 0,
this.gap = 1,
this.backgroundColor,
});
final Widget center;
final Widget? header;
final Widget? footer;
final Widget? left;
final Widget? right;
final double leftWidth;
final double rightWidth;
final double gap;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final theme = GarageTheme.of(context);
final divider = theme.colorScheme.divider;
Widget dock(Widget child, double width) =>
SizedBox(width: width, child: child);
Widget separator() => ColoredBox(
color: divider,
child: SizedBox(width: gap),
);
final workspace = Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (left != null && leftWidth > 0) ...[
dock(left!, leftWidth),
separator(),
],
Expanded(child: center),
if (right != null && rightWidth > 0) ...[
separator(),
dock(right!, rightWidth),
],
],
);
return ColoredBox(
color: backgroundColor ?? theme.colorScheme.background,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (header != null) header!,
Expanded(child: workspace),
if (footer != null) footer!,
],
),
);
}
}
/// Shared surface treatment for an editor's top and bottom chrome.
class ChromeBar extends StatelessWidget implements PreferredSizeWidget {
const ChromeBar({
super.key,
required this.child,
this.height,
this.padding = EdgeInsets.zero,
});
final Widget child;
/// Leave null to take [Density.chromeBarHeight], which is what keeps the
/// bar in step with the controls inside it. Was a hardcoded 30 that didnt
/// move with the tier at all, so at product density the bar came out
/// shorter than its own buttons.
final double? height;
final EdgeInsetsGeometry padding;
/// Without a BuildContext theres no density to ask, so this can only report
/// an explicit height. Nothing reads it today; the fallback matches the way
/// surface.dart's dividers handle the same problem.
@override
Size get preferredSize => Size.fromHeight(height ?? 0);
@override
Widget build(BuildContext context) {
final theme = GarageTheme.of(context);
return SizedBox(
height: height ?? theme.density.chromeBarHeight,
child: DecoratedBox(
decoration: BoxDecoration(color: theme.colorScheme.chrome),
child: Padding(padding: padding, child: child),
),
);
}
}
/// A dock surface with optional package-owned header and footer slots.
class DockPanel extends StatelessWidget {
const DockPanel({
super.key,
required this.child,
this.header,
this.footer,
this.padding = EdgeInsets.zero,
});
final Widget child;
final Widget? header;
final Widget? footer;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Panel(
borderRadius: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (header != null) header!,
Expanded(
child: Padding(padding: padding, child: child),
),
if (footer != null) footer!,
],
),
);
}
}
/// Garage editor layout: a main surface and two vertically stacked
/// sidebar surfaces separated by a draggable divider.
///
/// The shell owns the complete editor frame: header, workspace, footer, sizing,
/// hover highlighting, and the divider interaction. The application supplies
/// the header/footer content and the canvas, explorer, and properties widgets.
class GarageShell extends StatefulWidget {
const GarageShell({
super.key,
required this.main,
required this.sidebarTop,
required this.sidebarBottom,
this.header,
this.footer,
this.initialSidebarWidth = 320,
this.minPanelWidth = 100,
this.gap,
this.handleHeight = 200,
});
final Widget main;
final Widget sidebarTop;
final Widget sidebarBottom;
final Widget? header;
final Widget? footer;
final double initialSidebarWidth;
final double minPanelWidth;
/// Null uses the active theme's [ThemeData.panelGap], matching the original
/// Arcs & Angles Blender shell exactly.
final double? gap;
final double handleHeight;
@override
State<GarageShell> createState() => _GarageShellState();
}
class _GarageShellState extends State<GarageShell> {
late double _sidebarWidth = widget.initialSidebarWidth;
int? _hoveredPanel;
bool _dragging = false;
void _resizeSidebar(
DragUpdateDetails details,
double availableWidth,
double gap,
) {
final overhead = gap * 3;
final maxSidebarWidth = (availableWidth - overhead - widget.minPanelWidth)
.clamp(widget.minPanelWidth, double.infinity);
setState(() {
_sidebarWidth = (_sidebarWidth - details.delta.dx).clamp(
widget.minPanelWidth,
maxSidebarWidth,
);
});
}
Widget _panel(int id, Widget child) {
return MouseRegion(
onEnter: (_) {
if (_hoveredPanel != id) setState(() => _hoveredPanel = id);
},
onExit: (_) {
if (_hoveredPanel == id) setState(() => _hoveredPanel = null);
},
child: Panel(
active: _hoveredPanel == id,
clipBehavior: Clip.hardEdge,
child: child,
),
);
}
@override
Widget build(BuildContext context) {
final theme = GarageTheme.of(context);
final scheme = theme.colorScheme;
final gap = widget.gap ?? theme.panelGap;
final workspace = ColoredBox(
color: scheme.chrome,
child: LayoutBuilder(
builder: (context, constraints) {
final sidebarWidth = _sidebarWidth;
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(left: gap),
child: _panel(0, widget.main),
),
),
SizedBox(
width: gap,
child: Align(
alignment: Alignment.center,
child: SizedBox(
height: widget.handleHeight,
child: MouseRegion(
cursor: SystemMouseCursors.resizeColumn,
child: GestureDetector(
// pane splitter, not a control - keep it out of the
// semantics tree so it doesn't read as a bogus scrollable
excludeFromSemantics: true,
behavior: HitTestBehavior.translucent,
onHorizontalDragStart: (_) =>
setState(() => _dragging = true),
onHorizontalDragUpdate: (details) =>
_resizeSidebar(details, constraints.maxWidth, gap),
onHorizontalDragEnd: (_) =>
setState(() => _dragging = false),
onHorizontalDragCancel: () =>
setState(() => _dragging = false),
child: SizedBox(
width: gap,
child: Center(
child: _dragging
? Container(
width: 1.5,
color: scheme.mutedForeground,
)
: null,
),
),
),
),
),
),
),
Padding(
padding: EdgeInsets.only(right: gap),
child: SizedBox(
width: sidebarWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: _panel(1, widget.sidebarTop)),
SizedBox(height: gap),
Expanded(child: _panel(2, widget.sidebarBottom)),
],
),
),
),
],
);
},
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.header != null) widget.header!,
Expanded(child: workspace),
if (widget.footer != null) widget.footer!,
],
);
}
}