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.
Files changed (47) hide show
  1. package/README.md +208 -0
  2. package/app/app.vue +10 -0
  3. package/app/assets/css/tailwind.css +3 -0
  4. package/app/components/doc/component.vue +34 -0
  5. package/app/components/doc/megaSection.vue +56 -0
  6. package/app/components/doc/section.vue +56 -0
  7. package/app/pages/_docs.vue +2420 -0
  8. package/app/pages/index.vue +137 -0
  9. package/nuxt.config.ts +19 -0
  10. package/package.json +21 -0
  11. package/public/doc/avatar.avif +0 -0
  12. package/public/favicon.ico +0 -0
  13. package/public/robots.txt +2 -0
  14. package/tailwind.config.ts +28 -0
  15. package/tsconfig.json +18 -0
  16. package/ui-library/components/Accordion.vue +118 -0
  17. package/ui-library/components/Avatar.vue +121 -0
  18. package/ui-library/components/Badge.vue +116 -0
  19. package/ui-library/components/Breadcrumbs.vue +138 -0
  20. package/ui-library/components/Button.vue +154 -0
  21. package/ui-library/components/Card.vue +84 -0
  22. package/ui-library/components/Checkbox.vue +148 -0
  23. package/ui-library/components/CodeBlock.vue +99 -0
  24. package/ui-library/components/ColorPicker.vue +302 -0
  25. package/ui-library/components/Date.vue +240 -0
  26. package/ui-library/components/Dialog.vue +187 -0
  27. package/ui-library/components/Divider.vue +78 -0
  28. package/ui-library/components/Drawer.vue +67 -0
  29. package/ui-library/components/Dropdown.vue +248 -0
  30. package/ui-library/components/FileUploader.vue +330 -0
  31. package/ui-library/components/Icon.vue +82 -0
  32. package/ui-library/components/Image.vue +78 -0
  33. package/ui-library/components/Input.vue +531 -0
  34. package/ui-library/components/Radio.vue +356 -0
  35. package/ui-library/components/ScrollArea.vue +43 -0
  36. package/ui-library/components/Select.vue +309 -0
  37. package/ui-library/components/Stepper.vue +361 -0
  38. package/ui-library/components/TabPanel.vue +25 -0
  39. package/ui-library/components/Table.vue +152 -0
  40. package/ui-library/components/Tabs.vue +71 -0
  41. package/ui-library/components/Textarea.vue +233 -0
  42. package/ui-library/components/Toggle.vue +319 -0
  43. package/ui-library/components/Tooltip.vue +200 -0
  44. package/ui-library/components/plugin.ts +8 -0
  45. package/ui-library/components/uiProps.ts +11 -0
  46. package/ui-library/components/useUiClasses.ts +159 -0
  47. 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,2 @@
1
+ User-Agent: *
2
+ Disallow:
@@ -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>