diff --git a/packages/core/src/components/Tabs.ts b/packages/core/src/components/Tabs.ts new file mode 100644 index 0000000..9d874ad --- /dev/null +++ b/packages/core/src/components/Tabs.ts @@ -0,0 +1,156 @@ +import { + defineComponent, + h, + computed, + Comment, + Fragment, +} from '@vue/runtime-core' +import { ForegroundColor } from 'chalk' +import type { PropType, VNode } from '@vue/runtime-core' +import type { LiteralUnion } from '../utils' +import { TuiBox } from './Box' +import { TuiText } from './Text' +import { Styles } from '../renderer/styles' +import { onKeyData } from '../composables/keyboard' + +export const TuiTabs = defineComponent({ + props: { + modelValue: { + type: [String, Number], + required: true, + }, + flexDirection: { + type: String as PropType, + default: 'row', + }, + color: { + type: String as PropType>, + default: 'white', + }, + bgColor: { + type: String as PropType>, + default: 'black', + }, + activeColor: { + type: String as PropType>, + default: 'white', + }, + activeBgColor: { + type: String as PropType>, + default: 'blue', + }, + useTab: { + type: Boolean, + default: true, + }, + }, + emits: ['onChange', 'update:modelValue'], + setup(props, { emit, slots }) { + const isColumn = computed( + () => + props.flexDirection === 'column' || + props.flexDirection === 'column-reverse' + ) + + const keyMap = computed(() => ({ + previous: isColumn.value ? 'ArrowUp' : 'ArrowLeft', + next: isColumn.value ? 'ArrowDown' : 'ArrowRight', + })) + + const children = computed(() => { + const defaultSlots = slots.default?.() + const children = defaultSlots + ?.filter((child) => child.type !== Comment) + ?.reduce( + (nodeList: VNode[], node: VNode) => + node.type === Fragment + ? [...nodeList, ...(node.children as VNode[])] + : [...nodeList, node], + [] + ) + + return children ?? [] + }) + + function toggleTab(index: number) { + emit('update:modelValue', index) + emit('onChange', index) + } + + onKeyData( + [ + keyMap.value.previous, + keyMap.value.next, + ...(props.useTab ? ['Tab'] : []), + ], + ({ key }) => { + const step = { + [keyMap.value.previous]: -1, + [keyMap.value.next]: 1, + Tab: 1, + }[key] + const index = +props.modelValue + step + const toggleIndex = + index < 0 + ? children.value.length - 1 + : index > children.value.length - 1 + ? 0 + : index + toggleTab(toggleIndex) + } + ) + + function normalizeChild() { + return children.value.map((child, index) => { + const isActive = +props.modelValue === index + return h( + TuiBox, + { + flexGrow: 1, + }, + { + default: () => + h( + TuiText, + { + color: isActive ? props.activeColor : props.color, + bgColor: isActive ? props.activeBgColor : props.bgColor, + }, + { default: () => child } + ), + } + ) + }) + } + + return () => { + return h( + TuiBox, + { + flexDirection: props.flexDirection, + width: '100%', + height: '100%', + }, + { default: normalizeChild } + ) + } + }, +}) + +export const TuiTab = defineComponent({ + props: { + name: { + type: String, + default: '', + }, + }, + setup(props, { slots }) { + return () => { + return h(Fragment, {}, [ + h('tui:text', {}, ' '), + h('tui:text', {}, slots?.default?.() ?? props.name), + h('tui:text', {}, ' '), + ]) + } + }, +}) diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 392921c..f132e6a 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -5,4 +5,5 @@ export { TuiApp } from './App' export { TuiBox } from './Box' export { TuiLink } from './Link' +export { TuiTabs, TuiTab } from './Tabs' // export { default as TuiInput } from './Input.vue' diff --git a/packages/playground/components.d.ts b/packages/playground/components.d.ts index 8737c06..8aaf39d 100644 --- a/packages/playground/components.d.ts +++ b/packages/playground/components.d.ts @@ -12,6 +12,7 @@ declare module '@vue/runtime-core' { Div: typeof import('vue-termui')['TuiBox'] Link: typeof import('vue-termui')['TuiLink'] Span: typeof import('vue-termui')['TuiText'] + Tab: typeof import('vue-termui')['TuiTab'] Text: typeof import('vue-termui')['TuiText'] } } diff --git a/packages/playground/src/Tabs.vue b/packages/playground/src/Tabs.vue new file mode 100644 index 0000000..a5905e0 --- /dev/null +++ b/packages/playground/src/Tabs.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/playground/src/main.ts b/packages/playground/src/main.ts index bbaa6b2..5324db4 100644 --- a/packages/playground/src/main.ts +++ b/packages/playground/src/main.ts @@ -1,12 +1,13 @@ // import devtools from '@vue/devtools' // import devtools from '@vue/devtools/node' import { createApp } from 'vue-termui' -import App from './Focusables.vue' +// import App from './Focusables.vue' // import App from './Fragments.vue' // import App from './CenteredDemo.vue' // import App from './App.vue' // import App from './Counter.vue' // import App from './Borders.vue' +import App from './Tabs.vue' createApp(App, { // swapScreens: true, diff --git a/packages/vite-plugin-vue-termui/src/index.ts b/packages/vite-plugin-vue-termui/src/index.ts index 35dce1b..35ca554 100644 --- a/packages/vite-plugin-vue-termui/src/index.ts +++ b/packages/vite-plugin-vue-termui/src/index.ts @@ -169,6 +169,9 @@ export const VueTuiComponents = new Map([ ['a', 'TuiLink'], ['link', 'TuiLink'], + ['tabs', 'TuiTabs'], + ['tab', 'TuiTab'], + ['div', 'TuiBox'], ['box', 'TuiBox'],