off_ide 0.1.4
off_ide: ^0.1.4 copied to clipboard
A high-performance, VS Code-like workspace shell widget for Flutter applications, featuring split editors, tab management, and a customizable sidebar.
Off IDE #
A high-performance, VS Code-like workspace shell widget for Flutter applications.

Features #
| Feature | Description |
|---|---|
| VS Code Layout | Familiar structure with Activity Bar, Sidebar, and Editor Area |
| Split Editor | Vertical split panes to view multiple tabs side-by-side |
| Tab Management | Close actions, dirty state indicators, and drag-and-drop reordering |
| Unsaved Changes Guard | onBeforeCloseTab callback to prompt before closing dirty tabs |
| Keyboard Shortcuts | Browser-safe defaults (Alt+W, Alt+], Alt+[) with consumer overrides |
| Text Selection | SelectionArea wrapping for copy-paste in browser deployments |
| Persistence | Auto-restore open tabs and sidebar state via HydratedBloc |
| Web Ready | Responsive design, browser-safe shortcuts, and persistence support |
| High Performance | O(1) state lookups and granular rebuilds for large widget trees |
| Custom Icons | Dynamic iconWidget support (SVGs, images) with tree-shaking preserved |
| Customizable | Configurable activity bar, sidebar views, page registry, and shortcuts |
| Theme Aware | Integrates with ThemeData, supporting light and dark modes |
Getting Started #
Add off_ide to your pubspec.yaml:
dependencies:
off_ide: ^0.1.4
Usage #
The main entry point is the WorkspaceShell widget, which requires a WorkspaceConfig.
There are two primary ways to define this configuration: Hardcoded (for simple, static apps) and Dynamic (for robust, backend-driven apps like CRMs).
1. Basic Usage (Hardcoded) #
For simple applications where the sidebar structure never changes, you can define your WorkspaceConfig statically:
import 'package:flutter/material.dart';
import 'package:off_ide/off_ide.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: WorkspaceShell(
config: WorkspaceConfig(
// 1. Define Activity Bar Items
activityBarItems: [
const ActivityBarItem(
id: 'files',
icon: Icons.folder_outlined,
label: 'Explorer',
),
],
// 2. Define Sidebar Content
sidebarViews: {
'files': const SidebarView(
id: 'files',
title: 'EXPLORER',
groups: [
MenuGroup(
id: 'project',
label: 'My Project',
items: [
MenuItem(
id: 'readme',
label: 'README.md',
pageId: 'markdown_viewer',
icon: Icons.description_outlined,
),
],
),
],
),
},
// 3. Register Page Builders
pageRegistry: {
'markdown_viewer': (context, args) => const Center(child: Text('README')),
},
),
),
);
}
}
2. Unsaved Changes Guard #
Protect users from losing unsaved work by providing an onBeforeCloseTab callback:
WorkspaceConfig(
onBeforeCloseTab: (context, tab) async {
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Unsaved Changes'),
content: Text('"${tab.title}" has unsaved changes. Discard?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Discard')),
],
),
) ?? false;
},
// ... other config
)
Mark tabs dirty from your forms via the BLoC:
context.read<WorkspaceBloc>().add(MarkTabDirty(tabId: tabId, isDirty: true));
3. Keyboard Shortcuts #
Built-in browser-safe shortcuts (these don't conflict with browser hotkeys):
| Shortcut | Action |
|---|---|
Alt + W |
Close active tab |
Alt + ] |
Cycle to next tab |
Alt + [ |
Cycle to previous tab |
Add custom shortcuts via config:
WorkspaceConfig(
keyboardShortcuts: {
const SingleActivator(LogicalKeyboardKey.keyP, control: true):
() => showCommandPalette(context),
},
// ... other config
)
4. Advanced Usage: Dynamic Configuration & RBAC (Recommended) #
The WorkspaceConfig is built to be extremely flexible. For complex applications, it is highly recommended to dynamically generate your sidebar using backend-driven JSON schemas combined with a state manager (like flutter_bloc).
This approach easily enables Role-Based Access Control (RBAC).
Implementation Example
By wrapping WorkspaceShell in a state listener, the shell will seamlessly update and automatically close restricted tabs when roles change!
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Listen to your Auth/Role State Manager
return BlocBuilder<RoleCubit, String>(
builder: (context, currentRole) {
return MaterialApp(
home: WorkspaceShell(
config: WorkspaceConfig(
// 1. Parse your backend JSON and filter by `currentRole`
activityBarItems: CustomParser.parseActivityBar(jsonSchema, currentRole),
sidebarViews: CustomParser.parseSidebars(jsonSchema, currentRole),
// 2. Only provide page builders the user has access to
pageRegistry: CustomParser.getPageRegistry(currentRole),
),
),
);
},
);
}
}
Key Dynamic Features:
- Discard Restricted Items: Overwrite your
CustomParserto check if a user has permission to see a JSON item. If not, don't instantiate theMenuItem. - Auto-Purge Tabs: Because
WorkspaceShellis deeply stateful, if the user's role changes, the shell will automatically purge any open tabs that no longer exist in the updatedpageRegistry. - Custom Backend Icons: Make use of the
iconWidgetparameter to parse custom backend icons (SVGs, Image Assets, custom Flutter code) instead of standardIconDatato ensure Flutter web compilation works smoothly with tree-shaking.
See the example/lib/main.dart source code to view a complete, production-ready implementation of dynamic JSON parsing and an interactive RBAC role toggle.
Additional Resources #
Check out the example directory for a complete, runnable sample application.
License #
MIT