windly-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/app/app.vue +10 -0
- package/app/assets/css/tailwind.css +3 -0
- package/app/components/doc/component.vue +34 -0
- package/app/components/doc/megaSection.vue +56 -0
- package/app/components/doc/section.vue +56 -0
- package/app/pages/_docs.vue +2420 -0
- package/app/pages/index.vue +137 -0
- package/nuxt.config.ts +19 -0
- package/package.json +21 -0
- package/public/doc/avatar.avif +0 -0
- package/public/favicon.ico +0 -0
- package/public/robots.txt +2 -0
- package/tailwind.config.ts +28 -0
- package/tsconfig.json +18 -0
- package/ui-library/components/Accordion.vue +118 -0
- package/ui-library/components/Avatar.vue +121 -0
- package/ui-library/components/Badge.vue +116 -0
- package/ui-library/components/Breadcrumbs.vue +138 -0
- package/ui-library/components/Button.vue +154 -0
- package/ui-library/components/Card.vue +84 -0
- package/ui-library/components/Checkbox.vue +148 -0
- package/ui-library/components/CodeBlock.vue +99 -0
- package/ui-library/components/ColorPicker.vue +302 -0
- package/ui-library/components/Date.vue +240 -0
- package/ui-library/components/Dialog.vue +187 -0
- package/ui-library/components/Divider.vue +78 -0
- package/ui-library/components/Drawer.vue +67 -0
- package/ui-library/components/Dropdown.vue +248 -0
- package/ui-library/components/FileUploader.vue +330 -0
- package/ui-library/components/Icon.vue +82 -0
- package/ui-library/components/Image.vue +78 -0
- package/ui-library/components/Input.vue +531 -0
- package/ui-library/components/Radio.vue +356 -0
- package/ui-library/components/ScrollArea.vue +43 -0
- package/ui-library/components/Select.vue +309 -0
- package/ui-library/components/Stepper.vue +361 -0
- package/ui-library/components/TabPanel.vue +25 -0
- package/ui-library/components/Table.vue +152 -0
- package/ui-library/components/Tabs.vue +71 -0
- package/ui-library/components/Textarea.vue +233 -0
- package/ui-library/components/Toggle.vue +319 -0
- package/ui-library/components/Tooltip.vue +200 -0
- package/ui-library/components/plugin.ts +8 -0
- package/ui-library/components/uiProps.ts +11 -0
- package/ui-library/components/useUiClasses.ts +159 -0
- package/ui-library/module.ts +20 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<DocMegaSection
|
|
3
|
+
title="Mini WYSIWYG Editor"
|
|
4
|
+
sub-title="Demonstrates advanced Vue reactivity, history tracking, and DOM syncing."
|
|
5
|
+
class="mx-4"
|
|
6
|
+
>
|
|
7
|
+
<DocComponent
|
|
8
|
+
title="Reactive Editor with Undo/Redo"
|
|
9
|
+
caption="A minimal rich-text editor built using contenteditable and Vue's Composition API."
|
|
10
|
+
>
|
|
11
|
+
<div class="grid md:grid-cols-2 gap-6">
|
|
12
|
+
<div class="space-y-3">
|
|
13
|
+
<div class="flex gap-2">
|
|
14
|
+
<UIButton label="Undo" @click="undo" :disable="historyIndex <= 0" />
|
|
15
|
+
<UIButton label="Redo" @click="redo" :disable="historyIndex >= history.length - 1" />
|
|
16
|
+
<UIButton label="Clear" @click="clearEditor" color="red-400" />
|
|
17
|
+
<UIButton label="Copy HTML" @click="copyHTML" color="green-500" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div
|
|
21
|
+
ref="editorRef"
|
|
22
|
+
contenteditable
|
|
23
|
+
class="border rounded-md p-4 min-h-[200px] bg-white focus:outline-none"
|
|
24
|
+
@input="handleInput"
|
|
25
|
+
></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div>
|
|
29
|
+
<h4 class="font-semibold mb-2">Live HTML Output</h4>
|
|
30
|
+
<pre class="bg-gray-900 text-green-400 p-4 rounded-md text-sm overflow-auto">
|
|
31
|
+
{{ html }}
|
|
32
|
+
</pre>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</DocComponent>
|
|
36
|
+
</DocMegaSection>
|
|
37
|
+
</template>
|
|
38
|
+
<script setup>
|
|
39
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
40
|
+
|
|
41
|
+
const editorRef = ref(null)
|
|
42
|
+
const html = ref("")
|
|
43
|
+
|
|
44
|
+
const history = ref([""])
|
|
45
|
+
const historyIndex = ref(0)
|
|
46
|
+
|
|
47
|
+
const MAX_HISTORY = 30
|
|
48
|
+
|
|
49
|
+
let isProgrammaticUpdate = false
|
|
50
|
+
|
|
51
|
+
let debounceTimer = null
|
|
52
|
+
const debounce = (fn, delay = 300) => {
|
|
53
|
+
clearTimeout(debounceTimer)
|
|
54
|
+
debounceTimer = setTimeout(fn, delay)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleInput = () => {
|
|
58
|
+
if (isProgrammaticUpdate) return
|
|
59
|
+
|
|
60
|
+
debounce(() => {
|
|
61
|
+
const value = editorRef.value.innerHTML
|
|
62
|
+
|
|
63
|
+
if (value === history.value[historyIndex.value]) return
|
|
64
|
+
|
|
65
|
+
history.value = history.value.slice(0, historyIndex.value + 1)
|
|
66
|
+
|
|
67
|
+
history.value.push(value)
|
|
68
|
+
|
|
69
|
+
if (history.value.length > MAX_HISTORY) {
|
|
70
|
+
history.value.shift()
|
|
71
|
+
} else {
|
|
72
|
+
historyIndex.value++
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
historyIndex.value = history.value.length - 1
|
|
76
|
+
|
|
77
|
+
html.value = value
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const updateEditorFromHistory = () => {
|
|
82
|
+
const value = history.value[historyIndex.value]
|
|
83
|
+
|
|
84
|
+
isProgrammaticUpdate = true
|
|
85
|
+
|
|
86
|
+
html.value = value
|
|
87
|
+
editorRef.value.innerHTML = value
|
|
88
|
+
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
isProgrammaticUpdate = false
|
|
91
|
+
}, 0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const undo = () => {
|
|
95
|
+
if (historyIndex.value > 0) {
|
|
96
|
+
historyIndex.value--
|
|
97
|
+
updateEditorFromHistory()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const redo = () => {
|
|
102
|
+
if (historyIndex.value < history.value.length - 1) {
|
|
103
|
+
historyIndex.value++
|
|
104
|
+
updateEditorFromHistory()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const clearEditor = () => {
|
|
109
|
+
history.value = [""]
|
|
110
|
+
historyIndex.value = 0
|
|
111
|
+
updateEditorFromHistory()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const copyHTML = async () => {
|
|
115
|
+
await navigator.clipboard.writeText(html.value)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleKeydown = (e) => {
|
|
119
|
+
if (e.ctrlKey && e.key.toLowerCase() === "z") {
|
|
120
|
+
e.preventDefault()
|
|
121
|
+
undo()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (e.ctrlKey && e.key.toLowerCase() === "y") {
|
|
125
|
+
e.preventDefault()
|
|
126
|
+
redo()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onMounted(() => {
|
|
131
|
+
window.addEventListener("keydown", handleKeydown)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
onUnmounted(() => {
|
|
135
|
+
window.removeEventListener("keydown", handleKeydown)
|
|
136
|
+
})
|
|
137
|
+
</script>
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
|
+
import UiLibraryModule from './ui-library/module'
|
|
3
|
+
|
|
4
|
+
export default defineNuxtConfig({
|
|
5
|
+
modules: ['@nuxtjs/tailwindcss', UiLibraryModule],
|
|
6
|
+
css: ['~/assets/css/tailwind.css'],
|
|
7
|
+
compatibilityDate: '2025-07-15',
|
|
8
|
+
devtools: { enabled: true },
|
|
9
|
+
app: {
|
|
10
|
+
head: {
|
|
11
|
+
link: [
|
|
12
|
+
{
|
|
13
|
+
rel: 'stylesheet',
|
|
14
|
+
href: 'https://fonts.googleapis.com/icon?family=Material+Icons'
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "windly-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "nuxt build",
|
|
8
|
+
"dev": "nuxt dev",
|
|
9
|
+
"generate": "nuxt generate",
|
|
10
|
+
"preview": "nuxt preview",
|
|
11
|
+
"postinstall": "nuxt prepare"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"nuxt": "^4.2.2",
|
|
15
|
+
"vue": "^3.5.26",
|
|
16
|
+
"vue-router": "^4.6.4"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@nuxtjs/tailwindcss": "^6.14.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// tailwind.config.js
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
'./app/**/*.{vue,js,ts}',
|
|
5
|
+
'./ui-library/components/**/*.{vue,js,ts}'
|
|
6
|
+
],
|
|
7
|
+
safelist: [
|
|
8
|
+
'bg-white',
|
|
9
|
+
'text-white',
|
|
10
|
+
'border-white',
|
|
11
|
+
'ring-white',
|
|
12
|
+
'hover:bg-white',
|
|
13
|
+
'hover:text-white',
|
|
14
|
+
'bg-black',
|
|
15
|
+
'text-black',
|
|
16
|
+
'border-black',
|
|
17
|
+
'ring-black',
|
|
18
|
+
'hover:bg-black',
|
|
19
|
+
'hover:text-black',
|
|
20
|
+
{
|
|
21
|
+
pattern:
|
|
22
|
+
/(bg|text|border|ring|to|from)-(white|black|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)?/,
|
|
23
|
+
variants: ['hover', 'focus', 'active']
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
theme: { extend: {} },
|
|
27
|
+
plugins: []
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
// https://nuxt.com/docs/guide/concepts/typescript
|
|
3
|
+
"files": [],
|
|
4
|
+
"references": [
|
|
5
|
+
{
|
|
6
|
+
"path": "./.nuxt/tsconfig.app.json"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"path": "./.nuxt/tsconfig.server.json"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"path": "./.nuxt/tsconfig.shared.json"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "./.nuxt/tsconfig.node.json"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
type TriggerType = 'click' | 'hover'
|
|
5
|
+
type ToggleSide = 'left' | 'right'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
title?: string
|
|
10
|
+
caption?: string
|
|
11
|
+
modelValue?: boolean
|
|
12
|
+
defaultOpen?: boolean
|
|
13
|
+
trigger?: TriggerType
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
dense?: boolean
|
|
16
|
+
toggleSide?: ToggleSide
|
|
17
|
+
margin?: boolean
|
|
18
|
+
}>(),
|
|
19
|
+
{
|
|
20
|
+
defaultOpen: false,
|
|
21
|
+
trigger: 'click',
|
|
22
|
+
disabled: false,
|
|
23
|
+
dense: false,
|
|
24
|
+
toggleSide: 'right',
|
|
25
|
+
margin: true
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits < {
|
|
30
|
+
(e: 'update:modelValue', value: boolean): void
|
|
31
|
+
}> ()
|
|
32
|
+
|
|
33
|
+
const isControlled = computed(() => props.modelValue !== undefined)
|
|
34
|
+
const isOpen = ref < boolean > (props.defaultOpen ?? false)
|
|
35
|
+
|
|
36
|
+
watch(
|
|
37
|
+
() => props.modelValue,
|
|
38
|
+
val => {
|
|
39
|
+
if (isControlled.value && typeof val === 'boolean') {
|
|
40
|
+
isOpen.value = val
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{ immediate: true }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const setOpen = (val: boolean) => {
|
|
47
|
+
if (props.disabled) return
|
|
48
|
+
isOpen.value = val
|
|
49
|
+
emit('update:modelValue', val)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toggle = () => setOpen(!isOpen.value)
|
|
53
|
+
|
|
54
|
+
const headerEvents = computed<Record<string, (() => void) | undefined>>(() => {
|
|
55
|
+
if (props.trigger === 'hover') {
|
|
56
|
+
return {
|
|
57
|
+
mouseenter: () => setOpen(true),
|
|
58
|
+
mouseleave: () => setOpen(false),
|
|
59
|
+
click: undefined
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
mouseenter: undefined,
|
|
65
|
+
mouseleave: undefined,
|
|
66
|
+
click: toggle
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div class="flex flex-col rounded-md border transition-opacity" :class="[
|
|
74
|
+
dense ? 'text-sm' : '',
|
|
75
|
+
disabled ? 'opacity-50 pointer-events-none' : ''
|
|
76
|
+
]">
|
|
77
|
+
|
|
78
|
+
<!-- Header -->
|
|
79
|
+
<div v-on="headerEvents" class="flex items-center cursor-pointer select-none gap-3" :class="[
|
|
80
|
+
margin
|
|
81
|
+
? (dense ? 'p-2' : 'p-3')
|
|
82
|
+
: (dense ? 'py-2' : 'py-3'),
|
|
83
|
+
isOpen ? 'border-b' : ''
|
|
84
|
+
]">
|
|
85
|
+
|
|
86
|
+
<!-- Toggle (left) -->
|
|
87
|
+
<UIButton v-if="toggleSide === 'left'" icon="chevron_right" flat dense class="transition-transform"
|
|
88
|
+
:class="isOpen ? 'rotate-90' : ''" />
|
|
89
|
+
|
|
90
|
+
<!-- Header content -->
|
|
91
|
+
<div class="flex-1">
|
|
92
|
+
<slot name="header">
|
|
93
|
+
<h3 class="font-semibold">
|
|
94
|
+
{{ title }}
|
|
95
|
+
</h3>
|
|
96
|
+
<p v-if="caption" class="text-gray-500 text-xs">
|
|
97
|
+
{{ caption }}
|
|
98
|
+
</p>
|
|
99
|
+
</slot>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Toggle (right) -->
|
|
103
|
+
<UIButton v-if="toggleSide === 'right'" icon="chevron_right" flat dense class="transition-transform"
|
|
104
|
+
:class="isOpen ? 'rotate-90' : ''" />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Content -->
|
|
108
|
+
<transition enter-active-class="transition-all duration-300 ease-out"
|
|
109
|
+
leave-active-class="transition-all duration-200 ease-in" enter-from-class="opacity-0 max-h-0"
|
|
110
|
+
enter-to-class="opacity-100 max-h-[1000px]" leave-from-class="opacity-100 max-h-[1000px]"
|
|
111
|
+
leave-to-class="opacity-0 max-h-0">
|
|
112
|
+
<div v-show="isOpen" class="overflow-hidden pb-3" :class="margin ? 'px-3' : 'px-none'">
|
|
113
|
+
<slot />
|
|
114
|
+
</div>
|
|
115
|
+
</transition>
|
|
116
|
+
|
|
117
|
+
</div>
|
|
118
|
+
</template>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineOptions({
|
|
3
|
+
inheritAttrs: false,
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import { useUiClasses } from './useUiClasses'
|
|
8
|
+
import { uiProps } from "./uiProps";
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
image: { type: String, default: '' },
|
|
12
|
+
altText: { type: String, default: 'Avatar' },
|
|
13
|
+
initials: { type: String, default: '' },
|
|
14
|
+
borderColor: { type: String, default: '#fff' },
|
|
15
|
+
borderThickness: { type: Number, default: 2 },
|
|
16
|
+
shadow: { type: Boolean, default: false },
|
|
17
|
+
...uiProps
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Use composable
|
|
21
|
+
const {
|
|
22
|
+
// props
|
|
23
|
+
contentClass,
|
|
24
|
+
contentStyle,
|
|
25
|
+
textColor,
|
|
26
|
+
color,
|
|
27
|
+
size,
|
|
28
|
+
// computed
|
|
29
|
+
colorClass,
|
|
30
|
+
computedContentClass,
|
|
31
|
+
isColorDark,
|
|
32
|
+
isHex,
|
|
33
|
+
isTailwindColor
|
|
34
|
+
} = useUiClasses(props)
|
|
35
|
+
|
|
36
|
+
const sizeClass = computed(() => {
|
|
37
|
+
const map = {
|
|
38
|
+
sm: 'avatar-sm',
|
|
39
|
+
md: 'avatar-md',
|
|
40
|
+
lg: 'avatar-lg'
|
|
41
|
+
} as const
|
|
42
|
+
|
|
43
|
+
return map[size.value as keyof typeof map]
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const shadowClass = computed(() =>
|
|
47
|
+
props.shadow ? 'shadow-lg' : ''
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const avatarClass = computed(() => {
|
|
51
|
+
return [
|
|
52
|
+
computedContentClass.value,
|
|
53
|
+
sizeClass.value,
|
|
54
|
+
shadowClass.value,
|
|
55
|
+
isTailwindColor(textColor.value) ? `text-${textColor.value}` : `text-${isColorDark(color.value) ? 'white' : 'gray-700'}`,
|
|
56
|
+
isTailwindColor(color.value) ? `bg-${color.value}` : ""
|
|
57
|
+
].filter((c) => !["", "!"].includes(c))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const avatarStyle = computed(() => ({
|
|
61
|
+
backgroundColor: isHex(color.value) && color.value,
|
|
62
|
+
color: isHex(textColor.value) && textColor.value,
|
|
63
|
+
borderColor: props.borderColor,
|
|
64
|
+
borderWidth: `${props.borderThickness}px`,
|
|
65
|
+
borderStyle: 'solid',
|
|
66
|
+
...contentStyle.value
|
|
67
|
+
}))
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div class="avatar" :class="avatarClass" :style="avatarStyle">
|
|
72
|
+
<img v-if="image" :src="image" :alt="altText" class="avatar-image" />
|
|
73
|
+
<span v-else class="avatar-initials">
|
|
74
|
+
{{ initials || '..' }}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<style scoped>
|
|
80
|
+
.avatar {
|
|
81
|
+
display: flex;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
align-items: center;
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
position: relative;
|
|
87
|
+
text-align: center;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.avatar-sm {
|
|
91
|
+
width: 40px;
|
|
92
|
+
height: 40px;
|
|
93
|
+
font-size: 0.875rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.avatar-md {
|
|
97
|
+
width: 60px;
|
|
98
|
+
height: 60px;
|
|
99
|
+
font-size: 1rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.avatar-lg {
|
|
103
|
+
width: 80px;
|
|
104
|
+
height: 80px;
|
|
105
|
+
font-size: 1.25rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.avatar-image {
|
|
109
|
+
width: 100%;
|
|
110
|
+
height: 100%;
|
|
111
|
+
object-fit: cover;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.avatar-initials {
|
|
115
|
+
font-weight: bold;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.shadow-lg {
|
|
119
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineOptions({
|
|
3
|
+
inheritAttrs: false,
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
import { useSlots, computed } from 'vue'
|
|
7
|
+
import { useUiClasses } from './useUiClasses'
|
|
8
|
+
import { uiProps } from "./uiProps";
|
|
9
|
+
|
|
10
|
+
const slots = useSlots()
|
|
11
|
+
|
|
12
|
+
const props = defineProps({
|
|
13
|
+
content: { type: String, default: '' },
|
|
14
|
+
icon: { type: String, default: '' },
|
|
15
|
+
position: { type: String, default: 'superscript' },
|
|
16
|
+
show: { type: Boolean, default: true },
|
|
17
|
+
...uiProps
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Use composable
|
|
21
|
+
const {
|
|
22
|
+
// props
|
|
23
|
+
contentClass,
|
|
24
|
+
contentStyle,
|
|
25
|
+
color,
|
|
26
|
+
size,
|
|
27
|
+
rounded,
|
|
28
|
+
// computed
|
|
29
|
+
colorClass,
|
|
30
|
+
computedContentClass,
|
|
31
|
+
isColorDark,
|
|
32
|
+
isHex,
|
|
33
|
+
isTailwindColor
|
|
34
|
+
} = useUiClasses(props)
|
|
35
|
+
|
|
36
|
+
const hasSlots = computed(() => {
|
|
37
|
+
return !!slots.default?.()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const sizeClass = computed(() => {
|
|
41
|
+
const map = {
|
|
42
|
+
xs: 'w-auto min-w-2 h-2.5 text-[7px]',
|
|
43
|
+
sm: 'w-auto min-w-3 h-3 text-[10px]',
|
|
44
|
+
md: 'w-auto min-w-4 h-4 text-xs',
|
|
45
|
+
lg: 'w-auto min-w-6 h-6 text-sm'
|
|
46
|
+
} as const
|
|
47
|
+
|
|
48
|
+
return map[size.value as keyof typeof map]
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const roundedClass = computed(() => {
|
|
52
|
+
const map = {
|
|
53
|
+
xs: 'rounded-xs',
|
|
54
|
+
sm: 'rounded-sm',
|
|
55
|
+
md: 'rounded',
|
|
56
|
+
lg: 'rounded-lg',
|
|
57
|
+
full: 'rounded-full'
|
|
58
|
+
} as const
|
|
59
|
+
|
|
60
|
+
return map[rounded.value as keyof typeof map]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const positionClasses = computed(() =>
|
|
64
|
+
props.position === 'superscript' && hasSlots.value
|
|
65
|
+
? 'absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2'
|
|
66
|
+
: ''
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const badgeClass = computed(() => {
|
|
70
|
+
return [
|
|
71
|
+
'inline-flex items-center justify-center font-bold',
|
|
72
|
+
computedContentClass.value,
|
|
73
|
+
sizeClass.value,
|
|
74
|
+
roundedClass.value,
|
|
75
|
+
positionClasses.value,
|
|
76
|
+
isColorDark(color.value) ? 'text-white' : 'text-gray-700',
|
|
77
|
+
isTailwindColor(color.value) ? `bg-${color.value}` : ""
|
|
78
|
+
].filter((c) => !["", "!"].includes(c))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const badgeStyle = computed(() => {
|
|
82
|
+
const style = contentStyle.value
|
|
83
|
+
return {
|
|
84
|
+
backgroundColor: isHex(color.value) ? color.value : undefined,
|
|
85
|
+
...(typeof style === 'object' && style ? style : {})
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<div class="flex">
|
|
92
|
+
<div v-if="hasSlots" class="relative">
|
|
93
|
+
<slot />
|
|
94
|
+
<span v-if="show" :class="[
|
|
95
|
+
badgeClass
|
|
96
|
+
]" :style="[badgeStyle]">
|
|
97
|
+
<span v-if="icon" class="p-1 material-icons">{{ icon }}</span>
|
|
98
|
+
<span v-if="content" class="p-1">{{ content }}</span>
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div v-else>
|
|
102
|
+
<span v-if="show" :class="[
|
|
103
|
+
badgeClass
|
|
104
|
+
]" :style="[badgeStyle]">
|
|
105
|
+
<span v-if="icon" class="p-1 material-icons">{{ icon }}</span>
|
|
106
|
+
<span v-if="content" class="p-1">{{ content }}</span>
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
.material-icons {
|
|
114
|
+
font-size: 1em;
|
|
115
|
+
}
|
|
116
|
+
</style>
|