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 @@ +