First real content under app/. Lays the whole Tier 0 foundation in one
commit because the pieces depend on each other circularly (kernel →
extension → widgets → built-ins all reference types from the layer
below); splitting would leave intermediate commits that don't compile.
Key shapes:
* bare WidgetsApp root — no Material, no Cupertino, no Scaffold.
ClideTheme InheritedWidget is the only source of tokens.
* ClideKernel InheritedWidget aggregates 18 services (settings,
project, extensions, theme, panels, events, ipc, commands +
palette + keybindings, clipboard, files, notify, dialog, tray,
secrets, os, net, focus, log, i18n). ExtensionContext exposes
them through a stable interface.
* ClideExtension + sealed ContributionPoint hierarchy (Tab,
StatusItem, Toolbar, Command, TrayItem, LayoutPreset). One
manifest ships N contributions into kernel slots.
* Three-tier theme pipeline: palette (named colors) → semantic
roles → ~60 surface tokens. Defaults at each layer so legacy
palette-only themes produce a complete SurfaceTokens.
* A11y baked in from day one — every interactive primitive wraps
in Semantics(label:, hint:, button:); ensureSemantics() at boot;
theme/contrast.dart helper exposes token pairs for the WCAG
gate in the a11y test suite.
* i18n ported from fframe's L10n pattern (text-driven, namespaced
JSON catalogs, caller-supplied placeholders) with a proper
locale fallback chain (exact → language → default → placeholder)
fframe lacks.
* Four Tier-0 built-ins live (default-layout, welcome, ipc-status,
theme-picker) plus 17 id-reserving stubs so later tiers fill in
without rename churn.
.gitignore extended to cover app/ sub-package artefacts and the
Playwright harness scratch dirs.
Co-Authored-By: Claude <noreply@anthropic.com>
106 lines
3.2 KiB
Dart
106 lines
3.2 KiB
Dart
import 'package:clide_app/kernel/src/theme/controller.dart';
|
|
import 'package:clide_app/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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|