TeamPilot Tp design system — reusable Flutter UI primitives (Tp*) and theme tokens.
Wrap the app with TpTheme. Prefer TpTextStyles.of(context) for semantic text,
context.tpFonts for families, and TpGlyphWarmup for boot glyph shaping:
import 'package:shared_ui/shared_ui.dart';
MaterialApp(
theme: ThemeData(
extensions: [
TpFontTheme(
uiFontFamily: 'Noto Sans SC',
monoFontFamily: 'JetBrains Mono',
monoFontFamilyFallback: const ['monospace'],
),
],
),
builder: (context, child) {
return TpTheme(
data: TpThemeData.fromColorScheme(
Theme.of(context).colorScheme,
scale: 1.0, // layout spacing
iconScale: iconMultiplier,
controlScale: textMultiplier, // buttons/inputs track text size
),
child: child ?? const SizedBox.shrink(),
);
},
);
// Boot warmup — short probe per TextStyle fingerprint (not a charset dump):
final styles = TpGlyphWarmup.dedupeByShapeKey([
...TpTextStyles(theme).stylesForWarmup(),
...hostExtras,
]);
TpGlyphWarmup.shapeAll(styles: styles); // defaults to TpGlyphWarmup.styleProbeIn pubspec.yaml:
dependencies:
shared_ui:
path: packages/shared_uiimport 'package:shared_ui/shared_ui.dart';| Category | Examples |
|---|---|
| Button | TpButton, TpIconButton |
| Input | TpInput, TpInputFormField, TpTextarea, TpTextareaFormField |
| Token field | TpTokenTextField, TpTokenChipMirror, palette typedefs / edit helpers (applyTpTokenBackspace, …) |
| Select | TpSelect, TpSelectWithCustomInput, search / filter helpers |
| Dialog | TpDialog, showTpDialog, TpDialogPresentation, TpDialogPageShell, TpDialogNavShell |
| Form | TpForm, TpFormField, TpFormFieldLayout, TpFormMap |
| Overlay | TpPopover, TpTooltip, TpActionMenu / TpActionMenuPanel |
| Date range | TpDateRangePicker, TpRangeCalendar, calendar date utils |
| Toast | TpToast, TpToastWrapper, TpToastConfig, TpToastTheme, TpToastVariant, TpToastAction |
| Layout / chrome | TpCard, TpCardHeader, TpActionRow, TpSeparator, TpSegmentedControl, TpSegmentedPicker, TpEmptyState, TpHover / TpHoverRow (click cursor when interactive, animated hover fill; optional idle backgroundColor, enabled, onLongPress, onSecondaryTapDown, pressScale — prefer over bare MouseRegion+GestureDetector for onTap chrome; selected idle fill = pass backgroundColor, not a selected API on TpHover), TpSidebar |
| Deferred / keep-alive | TpDeferredMountShell, TpDeferredMountAfter, TpDeferredForegroundMount, TpKeepAliveLayer — host progressive paint guide (TeamPilot: docs/PERFORMANCE.md) |
| Preference | TpPreferenceRow, TpPreferenceStack, TpSectionHeader, TpDisclosure, TpStatusBadge, TpCompactSelect |
| File selection | showTpFileSelection, TpFileSelectionDeps, TpFileSelectionOptions, TpPickedEntry, port adapters (TpFilesystemPort, TpGalleryPort, …) |
| Theme | TpTheme, TpThemeData, TpTextStyles, TpFontTheme, TpGlyphWarmup, icon sizes (sm/md/lg/hero), spacing / typography / control metrics, per-component themes |
Toast engine sources live under lib/src/toast/engine/ and are not barrel-exported.
TpBreakpoints / TpBreakpoint provide Tailwind-aligned viewport width tokens (sm 640, md 768, lg 1024, xl 1280, xxl 1536) and predicates: up (mobile first, width >= token), down (width < token), only (half-open band [token, next); xxl is width >= 1536).
Width-scaled insets: TpWidthScale / TpScaledEdgeInsets / TpScaledDouble accept any non-empty subset of sm/md/lg/xl/xxl — omit unused stops and they lerp between neighbors (one stop = constant). Wrap a surface in TpWidthValueHost<T> and read with TpWidthValueScope.of<T>(context).
// Minimal: two stops
const pad = TpScaledEdgeInsets(
sm: EdgeInsets.all(8),
xxl: EdgeInsets.all(28),
);
// Or pin a single stop
const gap = TpScaledDouble(lg: 20);Shell hosts may still pass product-specific breakpoint widths (e.g. TeamPilot workspace shell 840 on TpSidebarProvider / dialogs). Do not replace those with TpBreakpoints.md blindly — use TpBreakpoints for component-level responsive layout inside a host-provided pane width.
Composable workspace sidebar: wrap the shell in TpSidebarProvider, place TpSidebar
beside TpSidebarInset (or use TpSidebarMenu / TpSidebarTrigger inside).
- State:
TpSidebarProviderowns desktopopen+widthand mobileopenMobile. Read or mutate viaTpSidebarScope.of(context)/maybeOf. - Mobile drawer: below
mobileBreakpoint(default768), the sidebar becomes a hidden overlay drawer opened byTpSidebarTriggeror edge drag. Close withTpSidebarScope.maybeOf(context)?.setOpenMobile(false)after navigation. - Overlay ownership: when several
TpSidebars share one provider (kept-alive home + workspace tabs), only the foreground instance setsoverlayActive: true. Losing ownership (true→false) closes sharedopenMobile; already-inactive hosts never mutate that flag. - Mobile drawer width (
TpSidebarTheme):widthMobileFraction— fraction of viewport width (default0.8).widthMobileOverride— optional fixed px; wins over fraction when set.resolveMobileDrawerWidth(screenWidth)— shared resolver for left drawer and host right overlays (widthMobileOverride ?? screenWidth * widthMobileFraction).widthMobile— legacy fixed default (288); not used byresolveMobileDrawerWidth.
- Hosts: TeamPilot relies on fraction (
widthMobileOverride: null). huji can pinwidthMobileOverride: 288until it opts into fraction. - Breakpoint: package default
768; TeamPilot passes840(WorkspacePanePolicy.narrowBreakpointWidth) onTpSidebarProvider,showTpDialog, andTpDialogNavShell.
TpSidebarProvider(
mobileBreakpoint: 840,
child: Row(
children: [
TpSidebar(child: /* menu */),
Expanded(child: TpSidebarInset(child: /* main */)),
],
),
);showTpDialog presents modal content as a centered card or a full-bleed page on narrow
viewports. Use TpDialogPresentation.card (default) for short confirms; use .page for
large management surfaces.
TpDialogPresentation.card—showDialog+ caller returnsTpDialog(or equivalent).TpDialogPresentation.page— belowmobileBreakpoint,showGeneralDialogmounts a zero-inset fullscreenMaterialsurface; on wide, wraps content in a constrainedTpDialog.- Chrome ownership:
showTpDialogdoes not add an app bar. Callers choose:TpDialogPageShell— simple pages on narrow and wide: mobile nav on narrow;TpDialogHeader+ theme content padding on wide. Pass the samemobileBreakpointasshowTpDialog. UsefillBody: truewhen the child uses verticalExpanded(lists / pinned footers); default shrink-wrap requires an intrinsic-height child (Column(min)). Do not use an outerSingleChildScrollViewon wide — it expands tomaxHeight. Scroll on narrow Expanded body, or usefillBody: truewith an inner scroll region.TpDialogNavShell— dual-pane nav + detail; owns narrow nav/detail bars. Never wrapTpDialogNavShellinTpDialogPageShell.
// Simple page (settings list, editor form, …)
showTpDialog<void>(
context: context,
presentation: TpDialogPresentation.page,
mobileBreakpoint: 840,
builder: (ctx) => TpDialogPageShell(
title: 'Automations',
mobileBreakpoint: 840,
fillBody: true, // list / Expanded child
child: AutomationsBody(),
),
);
// Dual-pane settings (nav list → detail on narrow)
showTpDialog<void>(
context: context,
presentation: TpDialogPresentation.page,
mobileBreakpoint: 840,
builder: (ctx) => TpDialogNavShell(
mobileBreakpoint: 840,
navTitle: (ctx) => 'Settings',
entries: [/* TpDialogNavEntry … */],
),
);Cross-platform file / directory / media picker UI. Host apps wire platform I/O through
port adapters; shared_ui has no hard dependency on photo_manager,
permission_handler, or file_picker.
final result = await showTpFileSelection(
context: context,
deps: TpFileSelectionDeps(
filesystem: myFilesystemAdapter,
permission: myPermissionAdapter,
gallery: myGalleryAdapter, // optional — gallery tab
desktop: myDesktopPickerAdapter, // optional — native desktop dialog
preview: myPreviewAdapter, // optional — image/video preview
strings: myStrings, // app l10n mapped into TpFileSelectionStrings
isDesktop: () => Platform.isLinux || Platform.isMacOS || Platform.isWindows,
),
options: const TpFileSelectionOptions(
allowMultiple: true,
selectionMode: TpSelectionMode.files,
allowedExtensions: ['pdf', 'png'],
),
);
// null = user cancelled; non-null list = confirmed selectionReturn type: Future<List<TpPickedEntry>?>. Each TpPickedEntry carries path,
kind (TpPickedKind.file / directory), and optional displayName / mimeType.
TpFileSelectionDeps ports
| Port | Required | Role |
|---|---|---|
TpFilesystemPort (filesystem) |
yes | Browse roots, list directories, optional full-disk search (searchFiles) |
TpPermissionPort (permission) |
yes | Storage / gallery permission prompts (ensureStorageAccess, ensureGalleryAccess) |
TpFileSelectionStrings (strings) |
yes | All user-visible labels — map from app ARB / l10n |
bool Function() (isDesktop) |
yes | Desktop vs mobile routing |
TpGalleryPort (gallery) |
optional | Photo / video gallery tab; omit to hide gallery |
TpDesktopPickerPort (desktop) |
optional | Native OS picker on desktop; when set and isDesktop() is true, showTpFileSelection short-circuits to pickFiles / pickDirectory without pushing the full-page UI |
TpMediaPreviewPort (preview) |
optional | In-tab image / video preview; omit to disable preview actions |
Desktop routing: when isDesktop() returns true and desktop is non-null, the entry
point delegates to the native picker and never mounts TpFileSelectionPage. Mobile (or
desktop without desktop) opens the full filesystem / gallery page via Navigator.push.
Tests: TpFileSelectionStrings.english() supplies English placeholders; port fakes
live in test/components/file_selection/fake_file_selection_ports.dart.
Sources: lib/src/components/file_selection/ (ports, models, show_tp_file_selection.dart,
TpFileSelectionPage, tabs).
lib/src/components/—Tp*widgets by category (includingfile_selection/)lib/src/deferred/— progressive mount / keep-alive primitiveslib/src/theme/—TpTheme/TpThemeData, tokens, component themeslib/src/toast/engine/— private toast overlay engine (not public API)lib/shared_ui.dart— public barrel export