dissolve app/ into repo root (D-056)
Single Flutter package at the repo root. All code, tests, assets, and platform directories moved from app/ to root. Package renamed from clide_app to clide — all imports rewritten. Merged pubspec combines core (ffi) and app (flutter, yaml, xterm) dependencies. Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon lifecycle. 317 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum ClideButtonVariant { normal, primary, subtle }
|
||||
|
||||
class ClideButton extends StatefulWidget {
|
||||
const ClideButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.variant = ClideButtonVariant.normal,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
this.semanticLabel,
|
||||
this.semanticHint,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final ClideButtonVariant variant;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
/// Overrides [label] for screen readers (use when the visible label is
|
||||
/// an icon-only glyph or a noun that reads oddly when announced).
|
||||
final String? semanticLabel;
|
||||
|
||||
/// Screen-reader hint describing the button's effect. Optional.
|
||||
final String? semanticHint;
|
||||
|
||||
@override
|
||||
State<ClideButton> createState() => _ClideButtonState();
|
||||
}
|
||||
|
||||
class _ClideButtonState extends State<ClideButton> {
|
||||
bool _hovered = false;
|
||||
bool _pressed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final enabled = widget.onPressed != null;
|
||||
|
||||
Color bg;
|
||||
Color fg;
|
||||
switch (widget.variant) {
|
||||
case ClideButtonVariant.normal:
|
||||
bg = _pressed
|
||||
? tokens.buttonActiveBackground
|
||||
: _hovered
|
||||
? tokens.buttonHoverBackground
|
||||
: tokens.buttonBackground;
|
||||
fg = tokens.buttonForeground;
|
||||
case ClideButtonVariant.primary:
|
||||
bg = _pressed
|
||||
? tokens.panelActiveBorder
|
||||
: _hovered
|
||||
? tokens.buttonActiveBackground
|
||||
: tokens.buttonActiveBackground;
|
||||
fg = tokens.globalBackground;
|
||||
case ClideButtonVariant.subtle:
|
||||
bg = _hovered
|
||||
? tokens.listItemHoverBackground
|
||||
: tokens.listItemBackground;
|
||||
fg = tokens.listItemForeground;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: enabled,
|
||||
label: widget.semanticLabel ?? widget.label,
|
||||
hint: widget.semanticHint,
|
||||
onTap: enabled ? widget.onPressed : null,
|
||||
excludeSemantics: true,
|
||||
child: MouseRegion(
|
||||
cursor:
|
||||
enabled ? SystemMouseCursors.click : SystemMouseCursors.forbidden,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _pressed = true),
|
||||
onTapCancel: () => setState(() => _pressed = false),
|
||||
onTapUp: (_) {
|
||||
setState(() => _pressed = false);
|
||||
widget.onPressed?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border.all(color: tokens.buttonBorder, width: 1),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: ClideText(
|
||||
widget.label,
|
||||
color: fg,
|
||||
fontWeight: widget.variant == ClideButtonVariant.primary
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClideDivider extends StatelessWidget {
|
||||
const ClideDivider({
|
||||
super.key,
|
||||
this.axis = Axis.horizontal,
|
||||
this.thickness = 1,
|
||||
});
|
||||
|
||||
final Axis axis;
|
||||
final double thickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = ClideTheme.of(context).surface.dividerColor;
|
||||
return Container(
|
||||
width: axis == Axis.vertical ? thickness : null,
|
||||
height: axis == Axis.horizontal ? thickness : null,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Stateless painter producing a single-color icon. Every icon in
|
||||
/// [icons/] subclasses this; widgets wrap with [ClideIcon] for size +
|
||||
/// color-from-theme.
|
||||
abstract class ClideIconPainter {
|
||||
const ClideIconPainter();
|
||||
|
||||
/// Paint the icon into a unit square (0,0 .. 1,1).
|
||||
void paint(Canvas canvas, Color color);
|
||||
}
|
||||
|
||||
class ClideIcon extends StatelessWidget {
|
||||
const ClideIcon(
|
||||
this.painter, {
|
||||
super.key,
|
||||
this.size = 14,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final ClideIconPainter painter;
|
||||
final double size;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resolved = color ?? ClideTheme.of(context).surface.globalForeground;
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CustomPaint(
|
||||
size: Size(size, size),
|
||||
painter: _IconPainterAdapter(painter: painter, color: resolved),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconPainterAdapter extends CustomPainter {
|
||||
_IconPainterAdapter({required this.painter, required this.color});
|
||||
|
||||
final ClideIconPainter painter;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.save();
|
||||
canvas.scale(size.width, size.height);
|
||||
painter.paint(canvas, color);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _IconPainterAdapter old) =>
|
||||
old.painter != painter || old.color != color;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:clide/widgets/src/clide_tooltip.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClideIconRailItem {
|
||||
const ClideIconRailItem({
|
||||
required this.id,
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final ClideIconPainter icon;
|
||||
final String tooltip;
|
||||
}
|
||||
|
||||
class ClideIconRail extends StatefulWidget {
|
||||
const ClideIconRail({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.activeId,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
final List<ClideIconRailItem> items;
|
||||
final String? activeId;
|
||||
final ValueChanged<String> onSelect;
|
||||
|
||||
@override
|
||||
State<ClideIconRail> createState() => _ClideIconRailState();
|
||||
}
|
||||
|
||||
class _ClideIconRailState extends State<ClideIconRail> {
|
||||
String? _hoveredId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onExit: (_) => setState(() => _hoveredId = null),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (final item in widget.items)
|
||||
_RailButton(
|
||||
item: item,
|
||||
active: item.id == widget.activeId,
|
||||
hovered: item.id == _hoveredId,
|
||||
onHover: () => setState(() => _hoveredId = item.id),
|
||||
onTap: () => widget.onSelect(item.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RailButton extends StatelessWidget {
|
||||
const _RailButton({
|
||||
required this.item,
|
||||
required this.active,
|
||||
required this.hovered,
|
||||
required this.onHover,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final ClideIconRailItem item;
|
||||
final bool active;
|
||||
final bool hovered;
|
||||
final VoidCallback onHover;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final color = active
|
||||
? tokens.globalForeground
|
||||
: hovered
|
||||
? tokens.sidebarForeground
|
||||
: tokens.sidebarSectionHeader;
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: active,
|
||||
label: item.tooltip,
|
||||
child: ClideTooltip(
|
||||
message: item.tooltip,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => onHover(),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: active
|
||||
? tokens.tabActiveBorder
|
||||
: const Color(0x00000000),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ClideIcon(item.icon, size: 16, color: color),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/src/clide_text.dart';
|
||||
import 'package:clide/widgets/src/typography.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClidePalette extends StatefulWidget {
|
||||
const ClidePalette({super.key});
|
||||
|
||||
@override
|
||||
State<ClidePalette> createState() => _ClidePaletteState();
|
||||
}
|
||||
|
||||
class _ClidePaletteState extends State<ClidePalette> {
|
||||
final _input = TextEditingController();
|
||||
final _focus = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focus.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_input.dispose();
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final kernel = ClideKernel.of(context);
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return ListenableBuilder(
|
||||
listenable: kernel.palette,
|
||||
builder: (ctx, _) {
|
||||
if (!kernel.palette.isOpen) return const SizedBox.shrink();
|
||||
final filtered = kernel.palette.filtered();
|
||||
return Positioned(
|
||||
top: 60,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 480,
|
||||
constraints: const BoxConstraints(maxHeight: 360),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.dropdownBackground,
|
||||
border: Border.all(color: tokens.dropdownBorder),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x40000000),
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: EditableText(
|
||||
controller: _input,
|
||||
focusNode: _focus,
|
||||
style: TextStyle(
|
||||
fontFamily: clideMonoFamily,
|
||||
fontSize: clideFontMono,
|
||||
color: tokens.dropdownForeground,
|
||||
),
|
||||
cursorColor: tokens.globalFocus,
|
||||
backgroundCursorColor: tokens.globalFocus,
|
||||
maxLines: 1,
|
||||
onChanged: (v) => kernel.palette.setFilter(v),
|
||||
onSubmitted: (_) {
|
||||
if (filtered.isNotEmpty) {
|
||||
kernel.palette.invoke(filtered.first.command);
|
||||
_input.clear();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final cmd = filtered[i];
|
||||
return _PaletteItem(
|
||||
title: cmd.title ?? cmd.command,
|
||||
command: cmd.command,
|
||||
binding: cmd.defaultBinding,
|
||||
onTap: () {
|
||||
kernel.palette.invoke(cmd.command);
|
||||
_input.clear();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaletteItem extends StatefulWidget {
|
||||
const _PaletteItem({
|
||||
required this.title,
|
||||
required this.command,
|
||||
required this.onTap,
|
||||
this.binding,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String command;
|
||||
final String? binding;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
State<_PaletteItem> createState() => _PaletteItemState();
|
||||
}
|
||||
|
||||
class _PaletteItemState extends State<_PaletteItem> {
|
||||
bool _hover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hover = true),
|
||||
onExit: (_) => setState(() => _hover = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
color: _hover ? tokens.listItemHoverBackground : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideText(
|
||||
widget.title,
|
||||
color: tokens.listItemForeground,
|
||||
),
|
||||
),
|
||||
if (widget.binding != null)
|
||||
ClideText(
|
||||
widget.binding!,
|
||||
fontSize: clideFontCaption,
|
||||
fontFamily: clideMonoFamily,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'clide_divider.dart';
|
||||
import 'clide_icon.dart';
|
||||
import 'clide_text.dart';
|
||||
import 'icons/x.dart';
|
||||
import 'typography.dart';
|
||||
|
||||
/// Shared chrome for any pane that sits in a tab or split: a title
|
||||
/// strip at the top, an optional close button, and the pane body
|
||||
/// underneath. `ClidePtyView`, diff views, canvas tabs, graph tabs —
|
||||
/// everything with a "pane header" surface reuses this.
|
||||
class ClidePaneChrome extends StatelessWidget {
|
||||
const ClidePaneChrome({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.onClose,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
/// Primary label in the header — typically the pane kind + an
|
||||
/// abbreviated path / session name (`terminal — ~/clide`).
|
||||
final String title;
|
||||
|
||||
/// Optional secondary line (cwd hint, session id, status).
|
||||
final String? subtitle;
|
||||
|
||||
/// Icon or badge drawn before the title.
|
||||
final Widget? leading;
|
||||
|
||||
/// Main pane content.
|
||||
final Widget child;
|
||||
|
||||
/// If provided, renders an `x` close button on the right. Primary
|
||||
/// Claude panes deliberately pass `null` so the user can't hide the
|
||||
/// primary (D-041).
|
||||
final VoidCallback? onClose;
|
||||
|
||||
/// Extra trailing widgets (status indicator, menu button, etc.).
|
||||
/// Drawn before the close button when both are present.
|
||||
final List<Widget>? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return ColoredBox(
|
||||
color: tokens.panelBackground,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_Header(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
leading: leading,
|
||||
onClose: onClose,
|
||||
trailing: trailing,
|
||||
),
|
||||
const ClideDivider(),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CloseButton extends StatefulWidget {
|
||||
const _CloseButton({required this.onPressed});
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
State<_CloseButton> createState() => _CloseButtonState();
|
||||
}
|
||||
|
||||
class _CloseButtonState extends State<_CloseButton> {
|
||||
bool _hover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Close pane',
|
||||
onTap: widget.onPressed,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hover = true),
|
||||
onExit: (_) => setState(() => _hover = false),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.onPressed,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _hover ? tokens.tabCloseHover : null,
|
||||
),
|
||||
child: ClideIcon(
|
||||
const CloseIcon(),
|
||||
size: 10,
|
||||
color: tokens.panelHeaderForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.leading,
|
||||
required this.onClose,
|
||||
required this.trailing,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget? leading;
|
||||
final VoidCallback? onClose;
|
||||
final List<Widget>? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
label: 'pane header: $title',
|
||||
child: ColoredBox(
|
||||
color: tokens.panelHeader,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null) ...[leading!, const SizedBox(width: 6)],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClideText(
|
||||
title,
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.panelHeaderForeground,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (subtitle != null)
|
||||
ClideText(
|
||||
subtitle!,
|
||||
fontSize: clideFontCaption,
|
||||
muted: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...trailing!.map(
|
||||
(w) => Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: w,
|
||||
),
|
||||
),
|
||||
if (onClose != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: _CloseButton(onPressed: onClose!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||
import 'package:clide/widgets/src/typography.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
/// Theme-aware terminal view. Wraps xterm.dart's [TerminalView] with
|
||||
/// clide token bindings, JetBrainsMono as the face, and a Semantics
|
||||
/// wrapper that exposes the pane as a live region with the terminal's
|
||||
/// aria label.
|
||||
///
|
||||
/// Callers provide the [Terminal] model; hooking its `onOutput` to an
|
||||
/// IPC `pane.write` call and feeding `pane.output` event bytes into
|
||||
/// `terminal.write()` is the consumer's job (typically a builtin
|
||||
/// extension — see `builtin.terminal` / `builtin.claude`).
|
||||
class ClidePtyView extends StatelessWidget {
|
||||
const ClidePtyView({
|
||||
super.key,
|
||||
required this.terminal,
|
||||
this.label,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.fontSize = clideFontMono,
|
||||
});
|
||||
|
||||
final Terminal terminal;
|
||||
|
||||
/// A11y label — typically the pane title ("terminal — ~/repo",
|
||||
/// "claude — primary", …).
|
||||
final String? label;
|
||||
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
final double fontSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
label: label,
|
||||
textField: true,
|
||||
multiline: true,
|
||||
focusable: true,
|
||||
liveRegion: true,
|
||||
child: ColoredBox(
|
||||
color: tokens.panelBackground,
|
||||
child: TerminalView(
|
||||
terminal,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
theme: _buildTheme(tokens),
|
||||
textStyle: TerminalStyle(
|
||||
fontSize: fontSize,
|
||||
fontFamily: clideMonoFamily,
|
||||
fontFamilyFallback: clideMonoFamilyFallback,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
backgroundOpacity: 1,
|
||||
cursorType: TerminalCursorType.block,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive an xterm [TerminalTheme] from our surface tokens.
|
||||
///
|
||||
/// Foreground / background pull from the editor tokens so the terminal
|
||||
/// visually matches the rest of the IDE. The 16-color ANSI palette is
|
||||
/// chosen to read well on our current two bundled themes
|
||||
/// (summer-night + whatever else ships); a future pass lets themes
|
||||
/// override the palette directly in their YAML.
|
||||
TerminalTheme _buildTheme(SurfaceTokens t) {
|
||||
// Selection tint derived from the focus accent at 40% alpha — no
|
||||
// dedicated token yet; revisit when the theme layer grows an
|
||||
// editor.selection.* token family.
|
||||
final selection = t.globalFocus.withAlpha(0x66);
|
||||
return TerminalTheme(
|
||||
cursor: t.globalForeground,
|
||||
selection: selection,
|
||||
foreground: t.globalForeground,
|
||||
background: t.panelBackground,
|
||||
// ANSI palette — reasonable defaults tuned for dark themes. The
|
||||
// bright variants are the same hue with higher luminance.
|
||||
black: const Color(0xFF1b1d23),
|
||||
red: const Color(0xFFe06c75),
|
||||
green: const Color(0xFF98c379),
|
||||
yellow: const Color(0xFFe5c07b),
|
||||
blue: const Color(0xFF61afef),
|
||||
magenta: const Color(0xFFc678dd),
|
||||
cyan: const Color(0xFF56b6c2),
|
||||
white: const Color(0xFFabb2bf),
|
||||
brightBlack: const Color(0xFF5c6370),
|
||||
brightRed: const Color(0xFFff7b85),
|
||||
brightGreen: const Color(0xFFabd486),
|
||||
brightYellow: const Color(0xFFffd89a),
|
||||
brightBlue: const Color(0xFF82c5ff),
|
||||
brightMagenta: const Color(0xFFdb8fe4),
|
||||
brightCyan: const Color(0xFF6fcbd6),
|
||||
brightWhite: const Color(0xFFffffff),
|
||||
searchHitBackground: const Color(0xFFffeb8c),
|
||||
searchHitBackgroundCurrent: const Color(0xFFffd54a),
|
||||
searchHitForeground: const Color(0xFF1b1d23),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Thin themed scrollbar. Tier-0 shell; more refined scrolling (velocity
|
||||
/// multiplier, keyboard nav) lands with the editor.
|
||||
class ClideScrollbar extends StatelessWidget {
|
||||
const ClideScrollbar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.child,
|
||||
this.axis = Axis.vertical,
|
||||
});
|
||||
|
||||
final ScrollController controller;
|
||||
final Widget child;
|
||||
final Axis axis;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return ScrollbarTheme(
|
||||
slider: tokens.scrollbarSlider,
|
||||
sliderHover: tokens.scrollbarSliderHover,
|
||||
track: tokens.scrollbarTrack,
|
||||
child: RawScrollbar(
|
||||
controller: controller,
|
||||
thumbColor: tokens.scrollbarSlider,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(4),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollbarTheme extends InheritedWidget {
|
||||
const ScrollbarTheme({
|
||||
super.key,
|
||||
required this.slider,
|
||||
required this.sliderHover,
|
||||
required this.track,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final Color slider;
|
||||
final Color sliderHover;
|
||||
final Color track;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ScrollbarTheme old) =>
|
||||
slider != old.slider ||
|
||||
sliderHover != old.sliderHover ||
|
||||
track != old.track;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClideSpine extends StatefulWidget {
|
||||
const ClideSpine({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onExpand,
|
||||
this.side = SpineSide.left,
|
||||
this.badgeCount = 0,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onExpand;
|
||||
final SpineSide side;
|
||||
final int badgeCount;
|
||||
|
||||
static const double width = 12;
|
||||
|
||||
@override
|
||||
State<ClideSpine> createState() => _ClideSpineState();
|
||||
}
|
||||
|
||||
enum SpineSide { left, right }
|
||||
|
||||
class _ClideSpineState extends State<ClideSpine> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final borderSide = BorderSide(color: tokens.dividerColor);
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: '${widget.label} — click to expand',
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onExpand,
|
||||
child: Container(
|
||||
width: ClideSpine.width,
|
||||
decoration: BoxDecoration(
|
||||
color: _hovered ? tokens.sidebarItemHover : tokens.sidebarBackground,
|
||||
border: Border(
|
||||
left: widget.side == SpineSide.right ? borderSide : BorderSide.none,
|
||||
right: widget.side == SpineSide.left ? borderSide : BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Transform.rotate(
|
||||
angle: widget.side == SpineSide.left ? -math.pi / 2 : math.pi / 2,
|
||||
child: Text(
|
||||
widget.label,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: tokens.globalTextMuted,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.badgeCount > 0)
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.statusInfo,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Themed container. Replaces Material's Card / Scaffold body surfaces
|
||||
/// for clide widgets — pulls color, border, and padding from the
|
||||
/// current theme's surface tokens.
|
||||
class ClideSurface extends StatelessWidget {
|
||||
const ClideSurface({
|
||||
super.key,
|
||||
this.child,
|
||||
this.color,
|
||||
this.border,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
final Color? border;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: color ?? tokens.panelBackground,
|
||||
border: border == null ? null : Border.all(color: border!, width: 1),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:clide/widgets/src/clide_text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
class ClideTabItem {
|
||||
const ClideTabItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final ClideIconPainter? icon;
|
||||
}
|
||||
|
||||
class ClideTabBar extends StatelessWidget {
|
||||
const ClideTabBar({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.activeId,
|
||||
required this.onSelect,
|
||||
this.height = 28,
|
||||
this.semanticContainerLabel,
|
||||
});
|
||||
|
||||
final List<ClideTabItem> items;
|
||||
final String? activeId;
|
||||
final ValueChanged<String> onSelect;
|
||||
final double height;
|
||||
|
||||
/// Optional container-level label for screen readers (e.g. "Sidebar tabs").
|
||||
final String? semanticContainerLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: semanticContainerLabel,
|
||||
explicitChildNodes: true,
|
||||
child: Container(
|
||||
height: height,
|
||||
color: tokens.tabBarBackground,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final item in items)
|
||||
_Tab(
|
||||
item: item,
|
||||
active: item.id == activeId,
|
||||
onTap: () => onSelect(item.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Tab extends StatefulWidget {
|
||||
const _Tab({required this.item, required this.active, required this.onTap});
|
||||
|
||||
final ClideTabItem item;
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
State<_Tab> createState() => _TabState();
|
||||
}
|
||||
|
||||
class _TabState extends State<_Tab> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final bg = widget.active
|
||||
? tokens.tabActive
|
||||
: (_hovered ? tokens.tabInactive : tokens.tabInactive);
|
||||
final fg = widget.active
|
||||
? tokens.tabActiveForeground
|
||||
: tokens.tabInactiveForeground;
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: widget.active,
|
||||
label: widget.item.title,
|
||||
onTap: widget.onTap,
|
||||
excludeSemantics: true,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: widget.active
|
||||
? tokens.tabActiveBorder
|
||||
: const Color(0x00000000),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.item.icon != null) ...[
|
||||
ClideIcon(widget.item.icon!, size: 12, color: fg),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
ClideText(widget.item.title, color: fg),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/typography.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Theme-aware Text. Defaults pull from the global foreground token
|
||||
/// and [clideUiDefaultWeight].
|
||||
///
|
||||
/// The UI font family is deliberately **not** set on this widget — it
|
||||
/// inherits from the ambient `DefaultTextStyle` which `_AppRoot`
|
||||
/// provides ([clideUiFamily] in real runs). Goldens rely on Alchemist
|
||||
/// injecting Ahem for deterministic metrics; hard-coding a family here
|
||||
/// would override that and break pixel determinism per D-024.
|
||||
class ClideText extends StatelessWidget {
|
||||
const ClideText(
|
||||
this.data, {
|
||||
super.key,
|
||||
this.color,
|
||||
this.fontSize = 14,
|
||||
this.fontFamily,
|
||||
this.fontWeight,
|
||||
this.muted = false,
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
this.textAlign,
|
||||
});
|
||||
|
||||
final String data;
|
||||
final Color? color;
|
||||
final double fontSize;
|
||||
final String? fontFamily;
|
||||
final FontWeight? fontWeight;
|
||||
final bool muted;
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final resolved =
|
||||
color ?? (muted ? tokens.globalTextMuted : tokens.globalForeground);
|
||||
return Text(
|
||||
data,
|
||||
maxLines: maxLines,
|
||||
overflow: overflow,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
color: resolved,
|
||||
fontSize: fontSize,
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: fontWeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Very small tooltip — hover a child to reveal a label. Uses an
|
||||
/// OverlayEntry to draw above everything else without Material.
|
||||
class ClideTooltip extends StatefulWidget {
|
||||
const ClideTooltip({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.child,
|
||||
this.showDelay = const Duration(milliseconds: 500),
|
||||
});
|
||||
|
||||
final String message;
|
||||
final Widget child;
|
||||
final Duration showDelay;
|
||||
|
||||
@override
|
||||
State<ClideTooltip> createState() => _ClideTooltipState();
|
||||
}
|
||||
|
||||
class _ClideTooltipState extends State<ClideTooltip> {
|
||||
OverlayEntry? _entry;
|
||||
bool _hovering = false;
|
||||
|
||||
void _show() {
|
||||
final overlay = Overlay.maybeOf(context);
|
||||
if (overlay == null) return;
|
||||
_entry?.remove();
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
final offset = box.localToGlobal(Offset(0, box.size.height + 4));
|
||||
_entry = OverlayEntry(
|
||||
builder: (ctx) {
|
||||
final tokens = ClideTheme.of(ctx).surface;
|
||||
return Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.tooltipBackground,
|
||||
border: Border.all(color: tokens.tooltipBorder),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: ClideText(widget.message, color: tokens.tooltipForeground),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
overlay.insert(_entry!);
|
||||
}
|
||||
|
||||
void _hide() {
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hide();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
tooltip: widget.message,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) async {
|
||||
_hovering = true;
|
||||
await Future<void>.delayed(widget.showDelay);
|
||||
if (mounted && _hovering) _show();
|
||||
},
|
||||
onExit: (_) {
|
||||
_hovering = false;
|
||||
_hide();
|
||||
},
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CheckIcon extends ClideIconPainter {
|
||||
const CheckIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.14
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.18, 0.52)
|
||||
..lineTo(0.42, 0.74)
|
||||
..lineTo(0.82, 0.30);
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ChevronRightIcon extends ClideIconPainter {
|
||||
const ChevronRightIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.12
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.36, 0.22)
|
||||
..lineTo(0.66, 0.50)
|
||||
..lineTo(0.36, 0.78);
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
|
||||
class ChevronDownIcon extends ClideIconPainter {
|
||||
const ChevronDownIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.12
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.22, 0.36)
|
||||
..lineTo(0.50, 0.66)
|
||||
..lineTo(0.78, 0.36);
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DotIcon extends ClideIconPainter {
|
||||
const DotIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
canvas.drawCircle(const Offset(0.5, 0.5), 0.18, Paint()..color = color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FolderIcon extends ClideIconPainter {
|
||||
const FolderIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08;
|
||||
final path = Path()
|
||||
..moveTo(0.08, 0.30)
|
||||
..lineTo(0.40, 0.30)
|
||||
..lineTo(0.48, 0.22)
|
||||
..lineTo(0.92, 0.22)
|
||||
..lineTo(0.92, 0.80)
|
||||
..lineTo(0.08, 0.80)
|
||||
..close();
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class GearIcon extends ClideIconPainter {
|
||||
const GearIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08
|
||||
..strokeCap = StrokeCap.round;
|
||||
const center = Offset(0.5, 0.5);
|
||||
const teeth = 8;
|
||||
const innerR = 0.25;
|
||||
const outerR = 0.40;
|
||||
|
||||
final path = Path();
|
||||
for (var i = 0; i < teeth; i++) {
|
||||
final a1 = (i / teeth) * 2 * math.pi;
|
||||
final a2 = ((i + 0.5) / teeth) * 2 * math.pi;
|
||||
final p1 = center + Offset(math.cos(a1) * innerR, math.sin(a1) * innerR);
|
||||
final p2 = center + Offset(math.cos(a1) * outerR, math.sin(a1) * outerR);
|
||||
final p3 = center + Offset(math.cos(a2) * outerR, math.sin(a2) * outerR);
|
||||
final p4 = center + Offset(math.cos(a2) * innerR, math.sin(a2) * innerR);
|
||||
path
|
||||
..moveTo(p1.dx, p1.dy)
|
||||
..lineTo(p2.dx, p2.dy)
|
||||
..lineTo(p3.dx, p3.dy)
|
||||
..lineTo(p4.dx, p4.dy);
|
||||
}
|
||||
canvas.drawPath(path, p);
|
||||
canvas.drawCircle(center, 0.12, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:ui';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
|
||||
class GitBranchIcon extends ClideIconPainter {
|
||||
const GitBranchIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08
|
||||
..strokeCap = StrokeCap.round;
|
||||
// Trunk line
|
||||
canvas.drawLine(const Offset(0.35, 0.2), const Offset(0.35, 0.8), paint);
|
||||
// Branch line
|
||||
canvas.drawLine(const Offset(0.65, 0.3), const Offset(0.65, 0.5), paint);
|
||||
canvas.drawLine(const Offset(0.65, 0.5), const Offset(0.35, 0.6), paint);
|
||||
// Dots at nodes
|
||||
final dot = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(const Offset(0.35, 0.2), 0.06, dot);
|
||||
canvas.drawCircle(const Offset(0.35, 0.8), 0.06, dot);
|
||||
canvas.drawCircle(const Offset(0.65, 0.3), 0.06, dot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
|
||||
class PhosphorIconPainter extends ClideIconPainter {
|
||||
const PhosphorIconPainter(this.codePoint, {this.family = 'Phosphor'});
|
||||
|
||||
final int codePoint;
|
||||
final String family;
|
||||
|
||||
@override
|
||||
void paint(ui.Canvas canvas, ui.Color color) {
|
||||
final builder = ui.ParagraphBuilder(
|
||||
ui.ParagraphStyle(fontFamily: family, fontSize: 1.0, height: 1.0, textAlign: ui.TextAlign.center),
|
||||
)
|
||||
..pushStyle(ui.TextStyle(color: color, fontFamily: family))
|
||||
..addText(String.fromCharCode(codePoint));
|
||||
final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: 1.0));
|
||||
final dy = (1.0 - paragraph.height) / 2;
|
||||
canvas.drawParagraph(paragraph, ui.Offset(0, dy));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is PhosphorIconPainter && other.codePoint == codePoint && other.family == family;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(codePoint, family);
|
||||
}
|
||||
|
||||
abstract class PhosphorIcons {
|
||||
static const folder = PhosphorIconPainter(0xe24a);
|
||||
static const fileText = PhosphorIconPainter(0xe23a);
|
||||
static const gitBranch = PhosphorIconPainter(0xe278);
|
||||
static const gitCommit = PhosphorIconPainter(0xe27a);
|
||||
static const gitDiff = PhosphorIconPainter(0xe27c);
|
||||
static const gitPullRequest = PhosphorIconPainter(0xe282);
|
||||
static const magnifyingGlass = PhosphorIconPainter(0xe30c);
|
||||
static const terminal = PhosphorIconPainter(0xe47e);
|
||||
static const terminalWindow = PhosphorIconPainter(0xeae8);
|
||||
static const code = PhosphorIconPainter(0xe1bc);
|
||||
static const codeBlock = PhosphorIconPainter(0xeafe);
|
||||
static const pencilSimple = PhosphorIconPainter(0xe3b4);
|
||||
static const eye = PhosphorIconPainter(0xe220);
|
||||
static const eyeSlash = PhosphorIconPainter(0xe224);
|
||||
static const arrowsOutSimple = PhosphorIconPainter(0xe0a6);
|
||||
static const arrowsInSimple = PhosphorIconPainter(0xe09e);
|
||||
static const list = PhosphorIconPainter(0xe2f0);
|
||||
static const listChecks = PhosphorIconPainter(0xeadc);
|
||||
static const gear = PhosphorIconPainter(0xe270);
|
||||
static const puzzlePiece = PhosphorIconPainter(0xe596);
|
||||
static const keyboard = PhosphorIconPainter(0xe2d8);
|
||||
static const palette = PhosphorIconPainter(0xe6c8);
|
||||
static const warning = PhosphorIconPainter(0xe4e0);
|
||||
static const warningCircle = PhosphorIconPainter(0xe4e2);
|
||||
static const check = PhosphorIconPainter(0xe182);
|
||||
static const checkCircle = PhosphorIconPainter(0xe184);
|
||||
static const caretLeft = PhosphorIconPainter(0xe138);
|
||||
static const caretRight = PhosphorIconPainter(0xe13a);
|
||||
static const caretDown = PhosphorIconPainter(0xe136);
|
||||
static const caretUp = PhosphorIconPainter(0xe13c);
|
||||
static const graph = PhosphorIconPainter(0xeb58);
|
||||
static const treeStructure = PhosphorIconPainter(0xe67c);
|
||||
static const image = PhosphorIconPainter(0xe2ca);
|
||||
static const link = PhosphorIconPainter(0xe2e2);
|
||||
static const chatCircle = PhosphorIconPainter(0xe168);
|
||||
static const robot = PhosphorIconPainter(0xe762);
|
||||
static const ticket = PhosphorIconPainter(0xe490);
|
||||
static const lightbulb = PhosphorIconPainter(0xe2dc);
|
||||
static const notepad = PhosphorIconPainter(0xe63e);
|
||||
static const bookOpen = PhosphorIconPainter(0xe0e6);
|
||||
static const xMark = PhosphorIconPainter(0xe4f6);
|
||||
static const circlesFour = PhosphorIconPainter(0xe190);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Plug-shaped connection icon. Used by the ipc-status statusbar item
|
||||
/// and by future connection-state affordances.
|
||||
class PlugIcon extends ClideIconPainter {
|
||||
const PlugIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
// body
|
||||
final body = Path()
|
||||
..moveTo(0.30, 0.30)
|
||||
..lineTo(0.60, 0.30)
|
||||
..lineTo(0.60, 0.55)
|
||||
..arcToPoint(const Offset(0.30, 0.55),
|
||||
radius: const Radius.circular(0.15), clockwise: false)
|
||||
..close();
|
||||
canvas.drawPath(body, p);
|
||||
// cord
|
||||
canvas.drawLine(const Offset(0.45, 0.60), const Offset(0.45, 0.88), p);
|
||||
// prongs
|
||||
canvas.drawLine(const Offset(0.38, 0.18), const Offset(0.38, 0.30), p);
|
||||
canvas.drawLine(const Offset(0.52, 0.18), const Offset(0.52, 0.30), p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'dart:ui';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
|
||||
class SearchIcon extends ClideIconPainter {
|
||||
const SearchIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08
|
||||
..strokeCap = StrokeCap.round;
|
||||
canvas.drawCircle(const Offset(0.42, 0.42), 0.22, paint);
|
||||
canvas.drawLine(const Offset(0.58, 0.58), const Offset(0.78, 0.78), paint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'dart:ui';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
|
||||
class TerminalIcon extends ClideIconPainter {
|
||||
const TerminalIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08
|
||||
..strokeCap = StrokeCap.round;
|
||||
// Prompt chevron >_
|
||||
canvas.drawLine(const Offset(0.2, 0.3), const Offset(0.45, 0.5), paint);
|
||||
canvas.drawLine(const Offset(0.45, 0.5), const Offset(0.2, 0.7), paint);
|
||||
// Cursor line
|
||||
canvas.drawLine(const Offset(0.5, 0.7), const Offset(0.8, 0.7), paint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'dart:ui';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
|
||||
class WarningIcon extends ClideIconPainter {
|
||||
const WarningIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.5, 0.15)
|
||||
..lineTo(0.85, 0.8)
|
||||
..lineTo(0.15, 0.8)
|
||||
..close();
|
||||
canvas.drawPath(path, paint);
|
||||
// Exclamation
|
||||
canvas.drawLine(const Offset(0.5, 0.4), const Offset(0.5, 0.58), paint);
|
||||
final dot = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(const Offset(0.5, 0.68), 0.035, dot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CloseIcon extends ClideIconPainter {
|
||||
const CloseIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
canvas
|
||||
..drawLine(const Offset(0.22, 0.22), const Offset(0.78, 0.78), p)
|
||||
..drawLine(const Offset(0.78, 0.22), const Offset(0.22, 0.78), p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/// Typography constants shared across widgets.
|
||||
///
|
||||
/// Two bundled families:
|
||||
///
|
||||
/// - [clideUiFamily] — Josefin Sans, the application-wide UI face.
|
||||
/// Shipped as a variable font (weights 100-700) + italic companion;
|
||||
/// default weight is [clideUiDefaultWeight] (Light / `w300`).
|
||||
/// - [clideMonoFamily] — JetBrains Mono, for terminal panes, diff
|
||||
/// views, code editors, and any other monospace surface.
|
||||
///
|
||||
/// Fallback chains exist for web builds + harnesses where the bundled
|
||||
/// asset isn't picked up (rare, but possible during `flutter test` if
|
||||
/// asset fonts aren't declared in the harness).
|
||||
library;
|
||||
|
||||
import 'package:flutter/widgets.dart' show FontWeight;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI face — Josefin Sans
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The bundled application UI family. Always resolved first.
|
||||
const String clideUiFamily = 'JosefinSans';
|
||||
|
||||
/// Default weight for UI text. Josefin Sans reads well at Light; the
|
||||
/// rest of the design adjusts contrast and size to stay legible.
|
||||
const FontWeight clideUiDefaultWeight = FontWeight.w300;
|
||||
|
||||
/// System fallback chain for the UI face. Sans-serif humanist faces
|
||||
/// that sit close to Josefin's proportions, ordered by platform.
|
||||
const List<String> clideUiFamilyFallback = [
|
||||
// User system install of Josefin, if any.
|
||||
'Josefin Sans',
|
||||
// Platform humanist sans defaults.
|
||||
'Inter',
|
||||
'Helvetica Neue',
|
||||
'Helvetica',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Monospace face — JetBrains Mono
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The bundled monospace family. Always resolved first.
|
||||
const String clideMonoFamily = 'JetBrainsMono';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type scale — semantic sizes. Widgets inherit from the ambient
|
||||
// DefaultTextStyle (set at the app root). Only override when the
|
||||
// semantic role genuinely differs from body text. Prefer these
|
||||
// constants over bare numbers so the scale stays coherent.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const double clideFontBody = 15;
|
||||
const double clideFontCaption = 14;
|
||||
const double clideFontMono = 14;
|
||||
|
||||
/// System fallback chain. Ordered by platform prevalence + quality of
|
||||
/// programming-ligature / box-drawing coverage.
|
||||
const List<String> clideMonoFamilyFallback = [
|
||||
// macOS
|
||||
'SF Mono',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
// Linux — user system install under the canonical PostScript name
|
||||
'JetBrains Mono',
|
||||
'Fira Code',
|
||||
'Hack',
|
||||
'DejaVu Sans Mono',
|
||||
'Liberation Mono',
|
||||
// Windows
|
||||
'Cascadia Code',
|
||||
'Consolas',
|
||||
// Last resort
|
||||
'monospace',
|
||||
];
|
||||
Reference in New Issue
Block a user