diff --git a/apps/jaspr_pad/lib/adapters/html.dart b/apps/jaspr_pad/lib/adapters/html.dart
index 437164159..b37e6b3f1 100644
--- a/apps/jaspr_pad/lib/adapters/html.dart
+++ b/apps/jaspr_pad/lib/adapters/html.dart
@@ -2,6 +2,6 @@ import 'package:jaspr/jaspr.dart';
@Import.onWeb(
'dart:html',
- show: [#window, #document, #HtmlDocument, #Element, #CustomEvent, #MouseEvent, #Event, #InputElement],
+ show: [#window, #document, #HtmlDocument, #Element, #CustomEvent, #MouseEvent, #KeyboardEvent, #Event, #InputElement],
)
export 'html.imports.dart';
diff --git a/apps/jaspr_pad/lib/adapters/html.imports.dart b/apps/jaspr_pad/lib/adapters/html.imports.dart
index 2cd44f83f..8a8abbd05 100644
--- a/apps/jaspr_pad/lib/adapters/html.imports.dart
+++ b/apps/jaspr_pad/lib/adapters/html.imports.dart
@@ -17,6 +17,8 @@ export '../generated/imports/_web.dart'
CustomEventOrStubbed,
MouseEvent,
MouseEventOrStubbed,
+ KeyboardEvent,
+ KeyboardEventOrStubbed,
Event,
EventOrStubbed,
InputElement,
diff --git a/apps/jaspr_pad/lib/components/playground/editing/editor.dart b/apps/jaspr_pad/lib/components/playground/editing/editor.dart
index 89e8dbdff..525fe7dc8 100644
--- a/apps/jaspr_pad/lib/components/playground/editing/editor.dart
+++ b/apps/jaspr_pad/lib/components/playground/editing/editor.dart
@@ -1,9 +1,11 @@
import 'package:collection/collection.dart';
import 'package:jaspr/dom.dart' hide Position;
import 'package:jaspr/jaspr.dart';
+import 'package:jaspr_riverpod/jaspr_riverpod.dart';
import '../../../adapters/html.dart';
import '../../../models/api_models.dart';
+import '../../../providers/settings_provider.dart';
import '../../utils/node_reader.dart';
import 'codemirror_options.dart';
@Import.onWeb('package:codemirror/codemirror.dart', show: [#CodeMirror, #Doc, #Position])
@@ -43,6 +45,8 @@ class EditorState extends State {
Map docs = {};
+ ProviderSubscription? _keyMapSub;
+
@override
void initState() {
super.initState();
@@ -50,6 +54,19 @@ class EditorState extends State {
for (var doc in component.documents) //
doc.key: createDoc(doc.key, doc.source, doc.mode),
};
+
+ // Listen for keymap changes
+ _keyMapSub = context.listenManual(keyMapProvider, (_, keyMap) {
+ if (kIsWeb && _editor != null) {
+ _editor!.setKeyMap(keyMap);
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _keyMapSub?.close();
+ super.dispose();
}
void createEditor(ElementOrStubbed node) {
@@ -59,6 +76,15 @@ class EditorState extends State {
_editor?.dispose();
_editorElement = node;
_editor = CodeMirror.fromElement(_editorElement!, options: codeMirrorOptions);
+
+ // Apply current keymap after editor creation
+ if (kIsWeb) {
+ final keyMap = context.read(keyMapProvider);
+ if (keyMap != 'default') {
+ _editor!.setKeyMap(keyMap);
+ }
+ }
+
if (component.activeDoc != null) {
Future.microtask(() => _editor!.swapDoc(docs[component.activeDoc!]!));
}
diff --git a/apps/jaspr_pad/lib/components/playground/footer.dart b/apps/jaspr_pad/lib/components/playground/footer.dart
index 70d812509..b41d08179 100644
--- a/apps/jaspr_pad/lib/components/playground/footer.dart
+++ b/apps/jaspr_pad/lib/components/playground/footer.dart
@@ -1,5 +1,8 @@
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
+import 'package:jaspr_riverpod/jaspr_riverpod.dart';
+
+import '../../providers/settings_provider.dart';
class PlaygroundFooter extends StatelessComponent {
const PlaygroundFooter({super.key});
@@ -7,12 +10,22 @@ class PlaygroundFooter extends StatelessComponent {
@override
Component build(BuildContext context) {
return footer([
- // i(
- // id: 'keyboard-button',
- // classes: ['material-icons', 'footer-item'],
- // attributes: {'aria-hidden': 'true'},
- // [text('keyboard')],
- // ),
+ Builder(builder: (context) {
+ final isVim = context.watch(keyMapProvider) == 'vim';
+ return i(
+ id: 'keyboard-button',
+ classes: 'material-icons footer-item',
+ styles: isVim ? Styles(raw: {'color': '#4CAF50'}) : null,
+ attributes: {
+ 'aria-hidden': 'true',
+ 'title': isVim ? 'Vim mode enabled (Cmd+Shift+/ to toggle)' : 'Enable vim mode (Cmd+Shift+/ to toggle)',
+ },
+ events: {
+ 'click': (_) => toggleVimMode(context),
+ },
+ [.text('keyboard')],
+ );
+ }),
div(classes: 'footer-item', [
a(href: 'https://docs.jaspr.site', target: Target.blank, classes: 'footer-item', [.text('Jaspr Docs')]),
a(href: 'https://discord.gg/XGXrGEk4c6', target: Target.blank, classes: 'footer-item', [
diff --git a/apps/jaspr_pad/lib/components/playground/playground.dart b/apps/jaspr_pad/lib/components/playground/playground.dart
index f55bbfba8..215dd271e 100644
--- a/apps/jaspr_pad/lib/components/playground/playground.dart
+++ b/apps/jaspr_pad/lib/components/playground/playground.dart
@@ -1,8 +1,10 @@
import 'package:jaspr/jaspr.dart';
import 'package:jaspr_riverpod/jaspr_riverpod.dart';
+import '../../adapters/html.dart';
import '../../providers/logic_provider.dart';
import '../../providers/project_provider.dart';
+import '../../providers/settings_provider.dart';
import '../elements/dialog.dart';
import 'footer.dart';
import 'header.dart';
@@ -19,6 +21,14 @@ class Playground extends StatelessComponent {
Future(() => context.read(logicProvider).compileFiles());
}
}, fireImmediately: true);
+
+ // Global keyboard shortcut for vim toggle (Cmd/Ctrl+Shift+/)
+ document.onKeyDown.listen((event) {
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key == '/') {
+ event.preventDefault();
+ toggleVimMode(context);
+ }
+ });
}
return .fragment([PlaygroundHeader(), MainSection(), PlaygroundFooter(), DialogSlot(slotId: 'dialog')]);
diff --git a/apps/jaspr_pad/lib/generated/imports/_stubs.dart b/apps/jaspr_pad/lib/generated/imports/_stubs.dart
index 22150e101..fa2ed8cce 100644
--- a/apps/jaspr_pad/lib/generated/imports/_stubs.dart
+++ b/apps/jaspr_pad/lib/generated/imports/_stubs.dart
@@ -17,6 +17,8 @@ dynamic CustomEvent;
typedef CustomEventOrStubbed = dynamic;
dynamic MouseEvent;
typedef MouseEventOrStubbed = dynamic;
+dynamic KeyboardEvent;
+typedef KeyboardEventOrStubbed = dynamic;
dynamic Event;
typedef EventOrStubbed = dynamic;
dynamic InputElement;
diff --git a/apps/jaspr_pad/lib/generated/imports/_web.dart b/apps/jaspr_pad/lib/generated/imports/_web.dart
index a3fad6e10..b0e049bc4 100644
--- a/apps/jaspr_pad/lib/generated/imports/_web.dart
+++ b/apps/jaspr_pad/lib/generated/imports/_web.dart
@@ -12,6 +12,7 @@ import 'dart:html'
Element,
CustomEvent,
MouseEvent,
+ KeyboardEvent,
Event,
InputElement,
IFrameElement,
@@ -35,6 +36,7 @@ export 'dart:html'
Element,
CustomEvent,
MouseEvent,
+ KeyboardEvent,
Event,
InputElement,
IFrameElement,
@@ -59,6 +61,7 @@ typedef EventOrStubbed = Event;
typedef HtmlDocumentOrStubbed = HtmlDocument;
typedef IFrameElementOrStubbed = IFrameElement;
typedef InputElementOrStubbed = InputElement;
+typedef KeyboardEventOrStubbed = KeyboardEvent;
typedef MDCDialogOrStubbed = MDCDialog;
typedef MDCMenuOrStubbed = MDCMenu;
typedef MDCRippleOrStubbed = MDCRipple;
diff --git a/apps/jaspr_pad/lib/models/api_models.mapper.dart b/apps/jaspr_pad/lib/models/api_models.mapper.dart
index aa3f559b6..7370b9017 100644
--- a/apps/jaspr_pad/lib/models/api_models.mapper.dart
+++ b/apps/jaspr_pad/lib/models/api_models.mapper.dart
@@ -2,6 +2,7 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format off
// ignore_for_file: type=lint
+// ignore_for_file: invalid_use_of_protected_member
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
diff --git a/apps/jaspr_pad/lib/models/gist.mapper.dart b/apps/jaspr_pad/lib/models/gist.mapper.dart
index a18eeda34..7c7aef40e 100644
--- a/apps/jaspr_pad/lib/models/gist.mapper.dart
+++ b/apps/jaspr_pad/lib/models/gist.mapper.dart
@@ -2,6 +2,7 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format off
// ignore_for_file: type=lint
+// ignore_for_file: invalid_use_of_protected_member
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
diff --git a/apps/jaspr_pad/lib/models/project.mapper.dart b/apps/jaspr_pad/lib/models/project.mapper.dart
index ad3e2a007..97734ddfe 100644
--- a/apps/jaspr_pad/lib/models/project.mapper.dart
+++ b/apps/jaspr_pad/lib/models/project.mapper.dart
@@ -2,6 +2,7 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format off
// ignore_for_file: type=lint
+// ignore_for_file: invalid_use_of_protected_member
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
diff --git a/apps/jaspr_pad/lib/models/sample.mapper.dart b/apps/jaspr_pad/lib/models/sample.mapper.dart
index 3b5f586f4..96e054ec0 100644
--- a/apps/jaspr_pad/lib/models/sample.mapper.dart
+++ b/apps/jaspr_pad/lib/models/sample.mapper.dart
@@ -2,6 +2,7 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format off
// ignore_for_file: type=lint
+// ignore_for_file: invalid_use_of_protected_member
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
diff --git a/apps/jaspr_pad/lib/models/tutorial.mapper.dart b/apps/jaspr_pad/lib/models/tutorial.mapper.dart
index 38133523f..bbae02803 100644
--- a/apps/jaspr_pad/lib/models/tutorial.mapper.dart
+++ b/apps/jaspr_pad/lib/models/tutorial.mapper.dart
@@ -2,6 +2,7 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format off
// ignore_for_file: type=lint
+// ignore_for_file: invalid_use_of_protected_member
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
diff --git a/apps/jaspr_pad/lib/providers/settings_provider.dart b/apps/jaspr_pad/lib/providers/settings_provider.dart
new file mode 100644
index 000000000..14ba57476
--- /dev/null
+++ b/apps/jaspr_pad/lib/providers/settings_provider.dart
@@ -0,0 +1,23 @@
+import 'package:jaspr/jaspr.dart';
+import 'package:jaspr_riverpod/jaspr_riverpod.dart';
+import 'package:jaspr_riverpod/legacy.dart';
+
+import '../adapters/html.dart';
+
+/// Editor keymap setting. Valid values: 'default', 'vim', 'emacs', 'sublime'
+final keyMapProvider = StateProvider((ref) {
+ if (kIsWeb) {
+ return window.localStorage['editorKeyMap'] ?? 'default';
+ }
+ return 'default';
+});
+
+/// Updates keymap and persists to localStorage
+void toggleVimMode(BuildContext context) {
+ final current = context.read(keyMapProvider);
+ final next = current == 'vim' ? 'default' : 'vim';
+ context.read(keyMapProvider.notifier).state = next;
+ if (kIsWeb) {
+ window.localStorage['editorKeyMap'] = next;
+ }
+}
diff --git a/apps/jaspr_pad/web/main.template.html b/apps/jaspr_pad/web/main.template.html
index 45a6f6a09..7ddd2685d 100644
--- a/apps/jaspr_pad/web/main.template.html
+++ b/apps/jaspr_pad/web/main.template.html
@@ -43,6 +43,7 @@
+