| import 'package:jaspr/dom.dart'; |
| import 'package:jaspr/jaspr.dart'; |
| import 'package:web/web.dart' as web; |
| |
| import '../../../utils/provider.dart'; |
| import '../../../utils/styles.dart'; |
| import '../view_models/tabs_view_model.dart'; |
| |
| class EditorTabs extends StatelessComponent { |
| const EditorTabs({super.key}); |
| |
| @override |
| Component build(BuildContext context) { |
| final tabsViewModel = context.watch<TabsViewModel>(); |
| |
| return div(classes: 'editor-tabs', [ |
| for (final tab in tabsViewModel.openTabs) |
| div( |
| key: ValueKey('tab-${tab.path}'), |
| classes: [ |
| 'editor-tab', |
| if (tab.path == tabsViewModel.activeFile) 'active', |
| if (tab.hasUnsavedChanges) 'dirty', |
| ].join(' '), |
| attributes: { |
| 'title': tab.path, |
| 'tabindex': '0', |
| 'role': 'tab', |
| 'aria-selected': tab.path == tabsViewModel.activeFile ? 'true' : 'false', |
| }, |
| events: { |
| 'click': (_) { |
| tabsViewModel.switchFile(tab.path); |
| }, |
| 'keydown': (e) { |
| final ke = e as web.KeyboardEvent; |
| if (ke.key == 'Enter' || ke.key == ' ') { |
| tabsViewModel.switchFile(tab.path); |
| } |
| }, |
| }, |
| [ |
| span(classes: 'editor-tab-name', [.text(tab.name)]), |
| if (tab.hasUnsavedChanges) ...[ |
| const span(classes: 'editor-tab-dirty-dot', []), |
| button( |
| classes: 'editor-tab-save', |
| attributes: { |
| 'title': 'Save file', |
| 'aria-label': 'Save ${tab.name}', |
| }, |
| events: { |
| 'click': (e) { |
| e.stopPropagation(); |
| tabsViewModel.saveTab(tab.path); |
| }, |
| }, |
| [_buildSaveIcon()], |
| ), |
| ], |
| if (tabsViewModel.openTabs.length > 1) |
| button( |
| classes: 'editor-tab-close', |
| attributes: { |
| 'title': 'Close tab', |
| 'aria-label': 'Close ${tab.name}', |
| }, |
| events: { |
| 'click': (e) { |
| e.stopPropagation(); |
| tabsViewModel.closeTab(tab.path); |
| }, |
| }, |
| [const .text('x')], |
| ), |
| ], |
| ), |
| ]); |
| } |
| |
| Component _buildSaveIcon() { |
| return const svg( |
| classes: 'save-icon', |
| viewBox: '0 0 24 24', |
| attributes: { |
| 'width': '12', |
| 'height': '12', |
| 'aria-hidden': 'true', |
| 'fill': 'none', |
| 'stroke': 'currentColor', |
| 'stroke-width': '2.5', |
| 'stroke-linecap': 'round', |
| 'stroke-linejoin': 'round', |
| }, |
| [ |
| path(attributes: {'d': 'M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z'}, []), |
| path(attributes: {'d': 'M17 21v-8H7v8'}, []), |
| path(attributes: {'d': 'M7 3v5h8'}, []), |
| ], |
| ); |
| } |
| |
| @css |
| static List<StyleRule> get styles => [ |
| css('.editor-tab').styles( |
| display: .flex, |
| minWidth: 96.px, |
| maxWidth: 180.px, |
| padding: .only(left: 12.px, right: 8.px), |
| border: .only( |
| right: .solid( |
| color: colorBorder, |
| width: 1.px, |
| ), |
| top: .solid(color: Colors.transparent, width: 2.px), |
| ), |
| cursor: .pointer, |
| userSelect: .none, |
| transition: .combine([ |
| Transition('background', duration: 150.ms, curve: .ease), |
| Transition('color', duration: 150.ms, curve: .ease), |
| Transition('border-color', duration: 150.ms, curve: .ease), |
| ]), |
| alignItems: .center, |
| gap: .all(6.px), |
| color: colorOnSurfaceVariant, |
| backgroundColor: colorContainerLow, |
| ), |
| css('.editor-tab-close').styles( |
| display: .flex, |
| width: 18.px, |
| height: 18.px, |
| padding: .zero, |
| border: .none, |
| radius: .circular(4.px), |
| opacity: 0, |
| cursor: .pointer, |
| transition: .combine([ |
| Transition('background', duration: 150.ms, curve: .ease), |
| Transition('color', duration: 150.ms, curve: .ease), |
| Transition('opacity', duration: 150.ms, curve: .ease), |
| ]), |
| justifyContent: .center, |
| alignItems: .center, |
| flex: const .shrink(0), |
| color: colorOnSurfaceVariant, |
| fontSize: 13.px, |
| lineHeight: 1.em, |
| backgroundColor: Colors.transparent, |
| ), |
| css('.editor-tab-close:hover, .editor-tab-close:focus').styles( |
| outline: const Outline(style: .none), |
| color: colorError, |
| backgroundColor: colorError.withOpacity(0.18), |
| ), |
| css('.editor-tab-dirty-dot').styles( |
| width: 7.px, |
| height: 7.px, |
| radius: .circular(3.5.px), |
| transition: Transition('opacity', duration: 150.ms, curve: .ease), |
| flex: const .shrink(0), |
| backgroundColor: colorOnSurface, |
| ), |
| css('.editor-tab-name').styles( |
| minWidth: .zero, |
| overflow: .hidden, |
| flex: const Flex(grow: 1, basis: .zero), |
| fontFamily: const .list([ |
| FontFamilies.courierNew, |
| FontFamilies.monospace, |
| ]), |
| fontSize: 12.px, |
| textOverflow: .ellipsis, |
| whiteSpace: .noWrap, |
| ), |
| css('.editor-tab-save').styles( |
| display: .flex, |
| width: 18.px, |
| height: 18.px, |
| padding: .zero, |
| border: .none, |
| radius: .circular(4.px), |
| cursor: .pointer, |
| pointerEvents: .none, |
| transition: .combine([ |
| Transition('background', duration: 150.ms, curve: .ease), |
| Transition('color', duration: 150.ms, curve: .ease), |
| Transition('opacity', duration: 150.ms, curve: .ease), |
| ]), |
| justifyContent: .center, |
| alignItems: .center, |
| flex: const .shrink(0), |
| color: colorOnSurfaceVariant, |
| backgroundColor: Colors.transparent, |
| ), |
| css('.editor-tab-save:hover, .editor-tab-save:focus').styles( |
| outline: const Outline(style: .none), |
| color: colorOnSurface, |
| backgroundColor: colorPrimary.withOpacity(0.18), |
| ), |
| css('.editor-tab.active').styles( |
| border: .only( |
| top: .solid( |
| color: colorPrimary, |
| width: 2.px, |
| ), |
| ), |
| color: colorOnSurface, |
| backgroundColor: colorContainer, |
| ), |
| css('.editor-tab.dirty:not(:hover):not(:focus) .editor-tab-close').styles( |
| opacity: 0, |
| pointerEvents: .none, |
| ), |
| css('.editor-tab:hover .editor-tab-close, .editor-tab.active .editor-tab-close').styles( |
| opacity: 1, |
| ), |
| css('.editor-tab:hover .editor-tab-dirty-dot').styles( |
| opacity: 0, |
| pointerEvents: .none, |
| ), |
| css('.editor-tab:hover .editor-tab-save').styles( |
| opacity: 1, |
| pointerEvents: .auto, |
| ), |
| css('.editor-tab:hover').styles( |
| outline: const Outline(style: .none), |
| color: colorOnSurface, |
| backgroundColor: colorContainerHigh, |
| ), |
| ]; |
| } |