Skip to content

Commit dbd65b5

Browse files
authored
feat: style syntax (#30)
1 parent b38e279 commit dbd65b5

File tree

8 files changed

+229
-53
lines changed

8 files changed

+229
-53
lines changed

packages/core/src/components/Box.ts

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { h, FunctionalComponent } from '@vue/runtime-core'
1+
import { h, FunctionalComponent, computed } from '@vue/runtime-core'
22
import { Styles } from '../renderer/styles'
3+
import { transformClassToStyleProps } from '../style-syntax '
34

45
export interface TuiBoxProps extends Omit<Styles, 'textWrap'> {
56
/**
@@ -48,31 +49,70 @@ export interface TuiBoxProps extends Omit<Styles, 'textWrap'> {
4849
* Optional title to display.
4950
*/
5051
title?: string
52+
53+
/**
54+
* Style shortcuts.
55+
*/
56+
class?: string
5157
}
5258

5359
export const TuiBox: FunctionalComponent<TuiBoxProps> = (props, { slots }) => {
60+
const propsWithClasses = computed(() =>
61+
props.class
62+
? {
63+
...props,
64+
...transformClassToStyleProps(props.class),
65+
}
66+
: props
67+
)
68+
69+
const propsValue = propsWithClasses.value
70+
5471
return h(
5572
'tui:box',
5673
{
5774
style: {
58-
...props,
59-
60-
display: props.display ?? 'flex',
61-
flexDirection: props.flexDirection ?? 'row',
62-
flexGrow: props.flexGrow ?? 0,
63-
flexShrink: props.flexShrink ?? 1,
75+
...propsValue,
76+
display: propsValue.display ?? 'flex',
77+
flexDirection: propsValue.flexDirection ?? 'row',
78+
flexGrow: propsValue.flexGrow ?? 0,
79+
flexShrink: propsValue.flexShrink ?? 1,
6480

65-
marginLeft: props.marginLeft ?? props.marginX ?? props.margin ?? 0,
66-
marginRight: props.marginRight ?? props.marginX ?? props.margin ?? 0,
67-
marginTop: props.marginTop ?? props.marginY ?? props.margin ?? 0,
68-
marginBottom: props.marginBottom ?? props.marginY ?? props.margin ?? 0,
81+
marginLeft:
82+
propsValue.marginLeft ?? propsValue.marginX ?? propsValue.margin ?? 0,
83+
marginRight:
84+
propsValue.marginRight ??
85+
propsValue.marginX ??
86+
propsValue.margin ??
87+
0,
88+
marginTop:
89+
propsValue.marginTop ?? propsValue.marginY ?? propsValue.margin ?? 0,
90+
marginBottom:
91+
propsValue.marginBottom ??
92+
propsValue.marginY ??
93+
propsValue.margin ??
94+
0,
6995

70-
paddingLeft: props.paddingLeft ?? props.paddingX ?? props.padding ?? 0,
96+
paddingLeft:
97+
propsValue.paddingLeft ??
98+
propsValue.paddingX ??
99+
propsValue.padding ??
100+
0,
71101
paddingRight:
72-
props.paddingRight ?? props.paddingX ?? props.padding ?? 0,
73-
paddingTop: props.paddingTop ?? props.paddingY ?? props.padding ?? 0,
102+
propsValue.paddingRight ??
103+
propsValue.paddingX ??
104+
propsValue.padding ??
105+
0,
106+
paddingTop:
107+
propsValue.paddingTop ??
108+
propsValue.paddingY ??
109+
propsValue.padding ??
110+
0,
74111
paddingBottom:
75-
props.paddingBottom ?? props.paddingY ?? props.padding ?? 0,
112+
propsValue.paddingBottom ??
113+
propsValue.paddingY ??
114+
propsValue.padding ??
115+
0,
76116
},
77117
},
78118
{ default: slots.default }
@@ -82,6 +122,7 @@ export const TuiBox: FunctionalComponent<TuiBoxProps> = (props, { slots }) => {
82122
TuiBox.displayName = 'TuiBox'
83123
TuiBox.props = [
84124
'title',
125+
'class',
85126
'position',
86127
'top',
87128
'right',

packages/core/src/components/Text.ts

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import chalk, { ForegroundColor } from 'chalk'
2+
import { transformClassToStyleProps } from '../style-syntax '
23
import {
34
PropType,
45
h,
@@ -7,6 +8,7 @@ import {
78
onMounted,
89
onUpdated,
910
onUnmounted,
11+
computed,
1012
} from '@vue/runtime-core'
1113
import type { LiteralUnion } from '../utils'
1214
import type { Styles } from '../renderer/styles'
@@ -31,6 +33,41 @@ export interface TuiTextProps {
3133
wrap?: Styles['textWrap']
3234
}
3335

36+
function transform(props: TuiTextProps, text: string): string {
37+
if (props.dimmed) {
38+
text = chalk.dim(text)
39+
}
40+
if (props.color) {
41+
text = colorize(text, props.color, 'foreground')
42+
}
43+
44+
if (props.bgColor) {
45+
text = colorize(text, props.bgColor, 'background')
46+
}
47+
48+
if (props.bold) {
49+
text = chalk.bold(text)
50+
}
51+
52+
if (props.italic) {
53+
text = chalk.italic(text)
54+
}
55+
56+
if (props.underline) {
57+
text = chalk.underline(text)
58+
}
59+
60+
if (props.strikethrough) {
61+
text = chalk.strikethrough(text)
62+
}
63+
64+
if (props.inverse) {
65+
text = chalk.inverse(text)
66+
}
67+
68+
return text
69+
}
70+
3471
export const TuiText = defineComponent({
3572
name: 'TuiText',
3673

@@ -44,9 +81,19 @@ export const TuiText = defineComponent({
4481
strikethrough: Boolean,
4582
inverse: Boolean,
4683
wrap: String as PropType<Styles['textWrap']>,
84+
class: String,
4785
},
4886

4987
setup(props, { slots }) {
88+
const propsWithClasses = computed(() =>
89+
props.class
90+
? {
91+
...props,
92+
...transformClassToStyleProps(props.class),
93+
}
94+
: props
95+
)
96+
5097
const scheduleUpdate = inject(scheduleUpdateSymbol)!
5198

5299
onMounted(scheduleUpdate)
@@ -55,47 +102,14 @@ export const TuiText = defineComponent({
55102

56103
onUnmounted(scheduleUpdate)
57104

58-
function transform(text: string): string {
59-
if (props.dimmed) {
60-
text = chalk.dim(text)
61-
}
62-
if (props.color) {
63-
text = colorize(text, props.color, 'foreground')
64-
}
65-
66-
if (props.bgColor) {
67-
text = colorize(text, props.bgColor, 'background')
68-
}
69-
70-
if (props.bold) {
71-
text = chalk.bold(text)
72-
}
73-
74-
if (props.italic) {
75-
text = chalk.italic(text)
76-
}
77-
78-
if (props.underline) {
79-
text = chalk.underline(text)
80-
}
81-
82-
if (props.strikethrough) {
83-
text = chalk.strikethrough(text)
84-
}
85-
86-
if (props.inverse) {
87-
text = chalk.inverse(text)
88-
}
89-
90-
return text
91-
}
92-
93105
return () => {
106+
const propsWithClassesValue = propsWithClasses.value
94107
return h(
95108
'tui:text',
96109
{
97-
style: { ...defaultStyle, textWrap: props.wrap },
98-
internal_transform: transform,
110+
style: { ...defaultStyle, textWrap: propsWithClassesValue.wrap },
111+
internal_transform: (text: string) =>
112+
transform(propsWithClassesValue, text),
99113
},
100114
slots.default?.()
101115
)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
export const specialAliases: Record<string, Record<string, string>> = {
2+
// Box Component
3+
'flex-row': { flexDirection: 'row' },
4+
'flex-row-reverse': { flexDirection: 'row-reverse' },
5+
'flex-col': { flexDirection: 'column' },
6+
'flex-col-reverse': { flexDirection: 'column-reverse' },
7+
'items-start': { alignItems: 'flex-start' },
8+
'items-end': { alignItems: 'flex-end' },
9+
'items-center': { alignItems: 'center' },
10+
'items-stretch': { alignItems: 'stretch' },
11+
'self-start': { alignSelf: 'flex-start' },
12+
'self-end': { alignSelf: 'flex-end' },
13+
'self-center': { alignSelf: 'center' },
14+
'self-auto': { alignSelf: 'auto' },
15+
'justify-start': { justifyContent: 'flex-start' },
16+
'justify-end': { justifyContent: 'flex-end' },
17+
'justify-center': { justifyContent: 'center' },
18+
'justify-between': { justifyContent: 'space-between' },
19+
'justify-around': { justifyContent: 'space-around' },
20+
'border-single': { borderStyle: 'single' },
21+
'border-double': { borderStyle: 'double' },
22+
'border-round': { borderStyle: 'round' },
23+
'border-bold': { borderStyle: 'bold' },
24+
'border-single-double': { borderStyle: 'singleDouble' },
25+
'border-double-single': { borderStyle: 'doubleSingle' },
26+
'border-classic': { borderStyle: 'classic' },
27+
'border-arrow': { borderStyle: 'arrow' },
28+
29+
// Text Component
30+
'text-wrap': { wrap: 'wrap' },
31+
'text-end': { wrap: 'end' },
32+
'text-truncate': { wrap: 'truncate' },
33+
'text-truncate-end': { wrap: 'truncate-end' },
34+
'text-truncate-middle': { wrap: 'truncate-middle' },
35+
'text-truncate-start': { wrap: 'truncate-start' },
36+
}
37+
38+
export const aliases: Record<string, string> = {
39+
// Box Component
40+
top: 'top',
41+
right: 'right',
42+
bottom: 'bottom',
43+
left: 'left',
44+
m: 'margin',
45+
mx: 'marginX',
46+
my: 'marginY',
47+
mt: 'marginTop',
48+
mr: 'marginRight',
49+
mb: 'marginBottom',
50+
ml: 'marginLeft',
51+
p: 'padding',
52+
px: 'paddingX',
53+
py: 'paddingY',
54+
pt: 'paddingTop',
55+
pr: 'paddingRight',
56+
pb: 'paddingBottom',
57+
pl: 'paddingLeft',
58+
grow: 'flexGrow',
59+
shrink: 'flexShrink',
60+
basis: 'flexBasis',
61+
w: 'width',
62+
'min-w': 'minWidth',
63+
'max-w': 'maxWidth',
64+
h: 'height',
65+
'min-h': 'minHeight',
66+
'max-h': 'maxHeight',
67+
border: 'borderColor',
68+
69+
// Text Component
70+
bg: 'bgColor',
71+
text: 'color',
72+
}
73+
74+
export function isInSpecialAliases(selector: string): boolean {
75+
return selector in specialAliases
76+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { transformClassToStyleProps } from './transform'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { specialAliases, isInSpecialAliases, aliases } from './alias'
2+
3+
function parseAttribute(attr: string) {
4+
let dashIndex = attr.indexOf('-')
5+
if (dashIndex < 0) {
6+
return [attr, true] as const
7+
} else {
8+
let identifier = attr.slice(0, dashIndex)
9+
if (identifier === 'min' || identifier === 'max') {
10+
dashIndex = attr.indexOf('-', dashIndex + 1)
11+
identifier = attr.slice(0, dashIndex)
12+
}
13+
14+
return [identifier, attr.slice(dashIndex + 1)]
15+
}
16+
}
17+
18+
function normalizeValue(value: string | boolean | number) {
19+
return !isNaN(+value) && typeof value !== 'boolean' ? +value : value
20+
}
21+
22+
export function transformClassToStyleProps(classStr: string) {
23+
const props: Record<string, string | boolean | number> = {}
24+
25+
for (const token of classStr.split(' ')) {
26+
if (isInSpecialAliases(token)) {
27+
Object.assign(props, specialAliases[token])
28+
continue
29+
}
30+
31+
const [identifier, value] = parseAttribute(token)
32+
33+
props[aliases[identifier] || identifier] = normalizeValue(value)
34+
}
35+
36+
return props
37+
}

packages/playground/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
1414
Link: typeof import('vue-termui')['TuiLink']
1515
Newline: typeof import('vue-termui')['TuiNewline']
1616
Progressbar: typeof import('vue-termui')['TuiProgressBar']
17+
Span: typeof import('vue-termui')['TuiText']
1718
Text: typeof import('vue-termui')['TuiText']
1819
}
1920
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<div class="w-30 h-10 justify-center flex-col items-center border-round">
3+
<span class="bg-yellow bold">hello world</span>
4+
</div>
5+
</template>

packages/playground/src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { createApp } from 'vue-termui'
77
// import App from './App.vue'
88
// import App from './Counter.vue'
99
// import App from './Borders.vue'
10-
import App from './InputDemo.vue'
10+
// import App from './InputDemo.vue'
11+
import App from './ShortcutsDemo.vue'
1112

1213
createApp(App, {
1314
// swapScreens: true,

0 commit comments

Comments
 (0)