blob: 3abc5e3fda61352c7dd29aeed33bb9372fe017aa [file]
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,
),
];
}