pgo-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 +195 -0
- package/dist/InputSearch-CHSoQ7GH.js +155 -0
- package/dist/favicon.ico +0 -0
- package/dist/index-B7ko30VS.js +5899 -0
- package/dist/index-BKsLeoKP.js +4781 -0
- package/dist/index-BeW0KHDT.js +34237 -0
- package/dist/index-DjwGqWIf.js +5086 -0
- package/dist/index-jnIKSPXM.js +4949 -0
- package/dist/index.es.js +58 -0
- package/dist/index.umd.js +111 -0
- package/dist/pgo-ui.css +1 -0
- package/package.json +99 -0
- package/src/App.vue +369 -0
- package/src/assets/fonts/Faruma.ttf +0 -0
- package/src/assets/logo.png +0 -0
- package/src/components/examples/AppBarExample.vue +100 -0
- package/src/components/examples/AvatarExample.vue +47 -0
- package/src/components/examples/BannerExample.vue +287 -0
- package/src/components/examples/BaseInputExample.vue +25 -0
- package/src/components/examples/BreadcrumbExample.vue +53 -0
- package/src/components/examples/CardExample.vue +77 -0
- package/src/components/examples/ChipExample.vue +225 -0
- package/src/components/examples/DatePickerExample.vue +31 -0
- package/src/components/examples/DropdownExample.vue +84 -0
- package/src/components/examples/EditorExample.vue +200 -0
- package/src/components/examples/ExpansionPanelExample.vue +42 -0
- package/src/components/examples/FileUploadExample.vue +40 -0
- package/src/components/examples/FormExample.vue +121 -0
- package/src/components/examples/HugeTest.vue +8 -0
- package/src/components/examples/LayoutContainerExample.vue +80 -0
- package/src/components/examples/ModalExample.vue +82 -0
- package/src/components/examples/NavDrawerExample.vue +171 -0
- package/src/components/examples/NumberFieldExample.vue +145 -0
- package/src/components/examples/RadioButtonExample.vue +161 -0
- package/src/components/examples/SearchExample.vue +322 -0
- package/src/components/examples/SelectExample.vue +121 -0
- package/src/components/examples/StackedTableViewExample.vue +53 -0
- package/src/components/examples/TabExample.vue +336 -0
- package/src/components/examples/TableExample.vue +228 -0
- package/src/components/examples/TextFieldExample.vue +181 -0
- package/src/components/examples/TextareaExample.vue +173 -0
- package/src/components/examples/ThemeToggle.vue +50 -0
- package/src/components/examples/TimelineExample.vue +66 -0
- package/src/components/examples/TipTapEditorExample.vue +20 -0
- package/src/components/examples/TooltipExample.vue +53 -0
- package/src/components/examples/VueDatePickerShowcase.vue +214 -0
- package/src/components/examples/_DatePickerExample.vue +33 -0
- package/src/components/examples/__FormExample.vue +77 -0
- package/src/components/index.ts +25 -0
- package/src/components/pgo/AppBar.vue +348 -0
- package/src/components/pgo/Avatar.vue +139 -0
- package/src/components/pgo/Banner.vue +300 -0
- package/src/components/pgo/Breadcrumb.vue +103 -0
- package/src/components/pgo/Button.vue +171 -0
- package/src/components/pgo/Card.vue +179 -0
- package/src/components/pgo/ConfirmationModel.vue +32 -0
- package/src/components/pgo/DataTable.vue +845 -0
- package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
- package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
- package/src/components/pgo/DatePicker/types.ts +11 -0
- package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
- package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
- package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
- package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
- package/src/components/pgo/Dropdown.vue +296 -0
- package/src/components/pgo/DropdownItem.vue +40 -0
- package/src/components/pgo/Editor.vue +511 -0
- package/src/components/pgo/ExpansionPanel.vue +185 -0
- package/src/components/pgo/Footer.vue +39 -0
- package/src/components/pgo/HeroIcon.vue +124 -0
- package/src/components/pgo/LayoutContainer.vue +104 -0
- package/src/components/pgo/Main.vue +37 -0
- package/src/components/pgo/Modal.vue +261 -0
- package/src/components/pgo/NavDrawer.vue +127 -0
- package/src/components/pgo/NavDrawerItem.vue +161 -0
- package/src/components/pgo/NavigationDrawer.vue +850 -0
- package/src/components/pgo/OLDNavDrawer.vue +661 -0
- package/src/components/pgo/OldAppBar.vue +223 -0
- package/src/components/pgo/PApp.vue +102 -0
- package/src/components/pgo/Pagination.vue +242 -0
- package/src/components/pgo/Search copy.vue +310 -0
- package/src/components/pgo/Search.vue +411 -0
- package/src/components/pgo/StackedTableView.vue +167 -0
- package/src/components/pgo/Tab.vue +617 -0
- package/src/components/pgo/TestInput.vue +395 -0
- package/src/components/pgo/Timeline.vue +367 -0
- package/src/components/pgo/TimelineItem.vue +80 -0
- package/src/components/pgo/TipTapEditor.vue +315 -0
- package/src/components/pgo/Tooltip.NOTES.md +12 -0
- package/src/components/pgo/Tooltip.PROPS.md +21 -0
- package/src/components/pgo/Tooltip.vue +281 -0
- package/src/components/pgo/base/Base.vue +444 -0
- package/src/components/pgo/buttons/Chip.vue +324 -0
- package/src/components/pgo/buttons/ChipGroup.vue +224 -0
- package/src/components/pgo/buttons/Radio.vue +424 -0
- package/src/components/pgo/filters/FilterSection.vue +188 -0
- package/src/components/pgo/filters/Searchbar.vue +216 -0
- package/src/components/pgo/forms/DynamicForm.vue +45 -0
- package/src/components/pgo/forms/Form.vue +132 -0
- package/src/components/pgo/index.ts +89 -0
- package/src/components/pgo/inputs/Checkbox.vue +320 -0
- package/src/components/pgo/inputs/DatePicker.vue +395 -0
- package/src/components/pgo/inputs/FileUpload.vue +326 -0
- package/src/components/pgo/inputs/InputSearch.vue +194 -0
- package/src/components/pgo/inputs/NumberField.vue +243 -0
- package/src/components/pgo/inputs/Radio.vue +162 -0
- package/src/components/pgo/inputs/RadioGroup.vue +188 -0
- package/src/components/pgo/inputs/Select.vue +535 -0
- package/src/components/pgo/inputs/TextField.vue +194 -0
- package/src/components/pgo/inputs/Textarea.vue +181 -0
- package/src/index.js +81 -0
- package/src/main.js +12 -0
- package/src/pgo-components/__index.js +104 -0
- package/src/pgo-components/_index.js +31 -0
- package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
- package/src/pgo-components/assets/fonts/logo.png +0 -0
- package/src/pgo-components/composables/useTheme.js +10 -0
- package/src/pgo-components/directives/tooltip-directive.ts +393 -0
- package/src/pgo-components/lib/componentConfig.js +147 -0
- package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
- package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
- package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
- package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
- package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
- package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
- package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
- package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
- package/src/pgo-components/lib/drawerState.ts +3 -0
- package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
- package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
- package/src/pgo-components/lib/i18n/useI18n.js +35 -0
- package/src/pgo-components/lib/index.ts +38 -0
- package/src/pgo-components/pages/Component.vue +7 -0
- package/src/pgo-components/pages/ComponentRenderer.vue +99 -0
- package/src/pgo-components/pages/Home.vue +125 -0
- package/src/pgo-components/pages/ListView.vue +372 -0
- package/src/pgo-components/pages/Page1.vue +296 -0
- package/src/pgo-components/pages/_Page1.vue +180 -0
- package/src/pgo-components/plugins/SnackBar.vue +251 -0
- package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
- package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
- package/src/pgo-components/plugins/theme-plugin.js +114 -0
- package/src/pgo-components/plugins/types.ts +46 -0
- package/src/pgo-components/plugins/useSnackBar.js +11 -0
- package/src/pgo-components/plugins/useSnackBar.ts +21 -0
- package/src/pgo-components/plugins/validation-plugin.js +11 -0
- package/src/pgo-components/services/Entry.json +813 -0
- package/src/pgo-components/services/axios.js +54 -0
- package/src/pgo-components/services/data.json +90 -0
- package/src/pgo-components/services/person.json +260 -0
- package/src/pgo-components/services/toast.ts +44 -0
- package/src/pgo-components/styles/global.css +234 -0
- package/src/pgo-components/styles/reset.css +96 -0
- package/src/pgo-components/styles/tokens.css +18 -0
- package/src/pgo-components/styles/utilities/border-radius.css +57 -0
- package/src/pgo-components/styles/utilities/borders.css +85 -0
- package/src/pgo-components/styles/utilities/colors.css +38 -0
- package/src/pgo-components/styles/utilities/cursor.css +19 -0
- package/src/pgo-components/styles/utilities/display.css +78 -0
- package/src/pgo-components/styles/utilities/elevation.css +33 -0
- package/src/pgo-components/styles/utilities/flex.css +403 -0
- package/src/pgo-components/styles/utilities/float.css +41 -0
- package/src/pgo-components/styles/utilities/hover.css +9 -0
- package/src/pgo-components/styles/utilities/index.css +18 -0
- package/src/pgo-components/styles/utilities/opacity.css +27 -0
- package/src/pgo-components/styles/utilities/overflow.css +26 -0
- package/src/pgo-components/styles/utilities/palette.css +515 -0
- package/src/pgo-components/styles/utilities/position.css +14 -0
- package/src/pgo-components/styles/utilities/sizing.css +70 -0
- package/src/pgo-components/styles/utilities/spacing.css +578 -0
- package/src/pgo-components/styles/utilities/transitions.css +58 -0
- package/src/pgo-components/styles/utilities/typography.css +91 -0
- package/src/pgo-components/styles/utilities/z-index.css +11 -0
- package/src/pgo-components/tokens/index.js +337 -0
- package/src/router/index.js +88 -0
- package/src/shims-vue.d.ts +14 -0
- package/src/validations/validationRules.js +50 -0
- package/tailwind.config.js +73 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="containerRef"
|
|
4
|
+
:class="['relative', containerClass, width]"
|
|
5
|
+
>
|
|
6
|
+
<Base
|
|
7
|
+
:model-value="modelValue"
|
|
8
|
+
:label="label"
|
|
9
|
+
:hint="hint"
|
|
10
|
+
:persistent-hint="!!hint"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
:readonly="readonly"
|
|
13
|
+
:required="required"
|
|
14
|
+
:error="!!error || errorMessages.length > 0"
|
|
15
|
+
:error-messages="errorMessages"
|
|
16
|
+
:clearable="clearable && !loading"
|
|
17
|
+
:size="size"
|
|
18
|
+
:id="inputId"
|
|
19
|
+
:prepend="prepend"
|
|
20
|
+
:append="append"
|
|
21
|
+
:is-open="isFocused"
|
|
22
|
+
@click:clear="clear"
|
|
23
|
+
:bg="bg"
|
|
24
|
+
:border="border"
|
|
25
|
+
:text-color="textColor"
|
|
26
|
+
:rounded="rounded"
|
|
27
|
+
:dir="computedDir"
|
|
28
|
+
:lang="computedLang"
|
|
29
|
+
:width="width"
|
|
30
|
+
:rules="rules"
|
|
31
|
+
>
|
|
32
|
+
<template #control="{ attrs, events }">
|
|
33
|
+
<input
|
|
34
|
+
ref="inputRef"
|
|
35
|
+
v-bind="attrs"
|
|
36
|
+
v-on="events"
|
|
37
|
+
type="number"
|
|
38
|
+
:value="displayValue"
|
|
39
|
+
:placeholder="isFocused && (modelValue === '' || modelValue === null) ? placeholder : ''"
|
|
40
|
+
:class="inputClasses"
|
|
41
|
+
:min="min"
|
|
42
|
+
:max="max"
|
|
43
|
+
:step="step"
|
|
44
|
+
@input="handleInput"
|
|
45
|
+
@keydown.enter="handleEnter"
|
|
46
|
+
@focus="handleFocus"
|
|
47
|
+
@blur="handleBlur"
|
|
48
|
+
/>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<!-- Loading spinner in append slot -->
|
|
52
|
+
<template v-if="loading" #append>
|
|
53
|
+
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
54
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
55
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 714 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
56
|
+
</svg>
|
|
57
|
+
</template>
|
|
58
|
+
</Base>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { ref, computed, inject } from 'vue'
|
|
64
|
+
import Base from '../base/Base.vue'
|
|
65
|
+
import { roundedMap, sizes, iconSizes } from '../../pgo-components/lib/componentConfig'
|
|
66
|
+
|
|
67
|
+
const props = defineProps({
|
|
68
|
+
modelValue: { type: [String, Number, null], default: '' },
|
|
69
|
+
label: { type: [String, Object], default: '' },
|
|
70
|
+
placeholder: { type: String, default: '0' },
|
|
71
|
+
hint: { type: String, default: '' },
|
|
72
|
+
error: { type: String, default: '' },
|
|
73
|
+
errorMessages: { type: Array, default: () => [] },
|
|
74
|
+
disabled: { type: Boolean, default: false },
|
|
75
|
+
readonly: { type: Boolean, default: false },
|
|
76
|
+
required: { type: Boolean, default: false },
|
|
77
|
+
clearable: { type: Boolean, default: true },
|
|
78
|
+
loading: { type: Boolean, default: false },
|
|
79
|
+
prepend: { type: String, default: '' },
|
|
80
|
+
append: { type: String, default: '' },
|
|
81
|
+
rules: { type: Array, default: () => [] },
|
|
82
|
+
|
|
83
|
+
// number-specific
|
|
84
|
+
min: { type: [Number, String], default: undefined },
|
|
85
|
+
max: { type: [Number, String], default: undefined },
|
|
86
|
+
step: { type: [Number, String], default: '1' },
|
|
87
|
+
allowDecimal: { type: Boolean, default: true },
|
|
88
|
+
|
|
89
|
+
// Appearance
|
|
90
|
+
size: { type: String },
|
|
91
|
+
rounded: { type: String },
|
|
92
|
+
border: { type: String},
|
|
93
|
+
textColor: { type: String },
|
|
94
|
+
bg: { type: String },
|
|
95
|
+
containerClass: { type: String, default: '' },
|
|
96
|
+
id: { type: String, default: '' },
|
|
97
|
+
width: { type: String, default: 'w-full' },
|
|
98
|
+
|
|
99
|
+
// RTL/Lang support
|
|
100
|
+
dir: { type: String, default: '' },
|
|
101
|
+
lang: { type: String, default: '' },
|
|
102
|
+
|
|
103
|
+
// Props that might be passed but not used (to avoid warnings)
|
|
104
|
+
items: { type: Array, default: () => [] },
|
|
105
|
+
itemText: { type: String, default: 'text' },
|
|
106
|
+
itemValue: { type: String, default: 'value' },
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const emit = defineEmits([
|
|
110
|
+
'update:modelValue',
|
|
111
|
+
'input',
|
|
112
|
+
'change',
|
|
113
|
+
'focus',
|
|
114
|
+
'blur',
|
|
115
|
+
'clear',
|
|
116
|
+
'enter'
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
// Inject dir from parent Card (if exists)
|
|
120
|
+
const cardDir = inject('parentDir', '')
|
|
121
|
+
const cardLang = inject('parentLang', '')
|
|
122
|
+
|
|
123
|
+
// Use component's dir if provided, otherwise use card's dir
|
|
124
|
+
const computedDir = computed(() => props.dir || cardDir)
|
|
125
|
+
const computedLang = computed(() => props.lang || cardLang)
|
|
126
|
+
|
|
127
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
128
|
+
const containerRef = ref(null)
|
|
129
|
+
const isFocused = ref(false)
|
|
130
|
+
|
|
131
|
+
// Generate unique ID
|
|
132
|
+
const inputId = computed(() => props.id || `input-${Math.random().toString(36).substr(2, 9)}`)
|
|
133
|
+
|
|
134
|
+
// Input classes (match TextField)
|
|
135
|
+
const inputClasses = computed(() => [
|
|
136
|
+
'w-full bg-transparent outline-none border-none',
|
|
137
|
+
'placeholder:text-gray-400',
|
|
138
|
+
'focus:outline-none'
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
// displayValue keeps caret-friendly representation
|
|
142
|
+
const displayValue = computed(() => {
|
|
143
|
+
if (props.modelValue === null || props.modelValue === undefined || props.modelValue === '') return ''
|
|
144
|
+
return String(props.modelValue)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse input value to number or empty string.
|
|
149
|
+
* - If field empty => ''
|
|
150
|
+
* - If allowDecimal false => parseInt
|
|
151
|
+
* - Otherwise parseFloat
|
|
152
|
+
* - If parsed is NaN => keep as string to allow user editing (do not force)
|
|
153
|
+
*/
|
|
154
|
+
const parseValue = (val: string) => {
|
|
155
|
+
if (val === '') return ''
|
|
156
|
+
// Trim to avoid whitespace
|
|
157
|
+
const v = String(val).trim()
|
|
158
|
+
if (v === '') return ''
|
|
159
|
+
// If not allowed decimal and contains dot -> treat as parseInt of part before dot
|
|
160
|
+
if (!props.allowDecimal) {
|
|
161
|
+
const parsed = parseInt(v, 10)
|
|
162
|
+
return Number.isNaN(parsed) ? v : parsed
|
|
163
|
+
}
|
|
164
|
+
const parsedFloat = parseFloat(v)
|
|
165
|
+
return Number.isNaN(parsedFloat) ? v : parsedFloat
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const clampValue = (num: number) => {
|
|
169
|
+
let n = num
|
|
170
|
+
if (props.min !== undefined && props.min !== null && props.min !== '') {
|
|
171
|
+
const minN = Number(props.min)
|
|
172
|
+
if (!Number.isNaN(minN)) n = Math.max(n, minN)
|
|
173
|
+
}
|
|
174
|
+
if (props.max !== undefined && props.max !== null && props.max !== '') {
|
|
175
|
+
const maxN = Number(props.max)
|
|
176
|
+
if (!Number.isNaN(maxN)) n = Math.min(n, maxN)
|
|
177
|
+
}
|
|
178
|
+
return n
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const handleInput = (e: Event) => {
|
|
182
|
+
const target = e.target as HTMLInputElement
|
|
183
|
+
const raw = target.value
|
|
184
|
+
|
|
185
|
+
// Emit input for immediate reactive usage
|
|
186
|
+
emit('input', raw)
|
|
187
|
+
|
|
188
|
+
// Parse the value; if parse returns number, clamp to min/max
|
|
189
|
+
const parsed = parseValue(raw)
|
|
190
|
+
|
|
191
|
+
if (typeof parsed === 'number') {
|
|
192
|
+
const clamped = clampValue(parsed)
|
|
193
|
+
emit('update:modelValue', clamped)
|
|
194
|
+
// For consistency also emit change when user types a valid number
|
|
195
|
+
emit('change', clamped)
|
|
196
|
+
} else {
|
|
197
|
+
// Keep user's intermediate string (e.g. '-' or '.'), emit as-is to model so UI shows it
|
|
198
|
+
emit('update:modelValue', parsed)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const handleFocus = (event: Event) => {
|
|
203
|
+
isFocused.value = true
|
|
204
|
+
emit('focus', event)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const handleBlur = (event: Event) => {
|
|
208
|
+
isFocused.value = false
|
|
209
|
+
// On blur, if modelValue is numeric string, coerce to number and clamp
|
|
210
|
+
const mv = props.modelValue
|
|
211
|
+
if (typeof mv === 'string' && mv.trim() !== '') {
|
|
212
|
+
const coerced = parseValue(mv)
|
|
213
|
+
if (typeof coerced === 'number') {
|
|
214
|
+
const clamped = clampValue(coerced)
|
|
215
|
+
emit('update:modelValue', clamped)
|
|
216
|
+
emit('change', clamped)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
emit('blur', event)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const handleEnter = (event: Event) => {
|
|
223
|
+
emit('enter', props.modelValue)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const clear = () => {
|
|
227
|
+
emit('update:modelValue', '')
|
|
228
|
+
emit('clear')
|
|
229
|
+
inputRef.value?.focus()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Focus method
|
|
233
|
+
const focus = () => {
|
|
234
|
+
inputRef.value?.focus()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Expose methods
|
|
238
|
+
defineExpose({ focus, clear })
|
|
239
|
+
</script>
|
|
240
|
+
|
|
241
|
+
<style scoped>
|
|
242
|
+
/* no additional styling; Base handles visual chrome */
|
|
243
|
+
</style>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label
|
|
3
|
+
:class="['inline-flex gap-2 items-center', { 'opacity-50 cursor-not-allowed': disabled, 'cursor-pointer': !disabled }]"
|
|
4
|
+
>
|
|
5
|
+
<input
|
|
6
|
+
ref="inputRef"
|
|
7
|
+
v-bind="attrs"
|
|
8
|
+
class="sr-only"
|
|
9
|
+
type="radio"
|
|
10
|
+
:name="name"
|
|
11
|
+
:value="value"
|
|
12
|
+
:disabled="disabled"
|
|
13
|
+
:checked="isChecked"
|
|
14
|
+
@change="onChange"
|
|
15
|
+
@keydown.enter.prevent="onChange"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<!-- Outer circle -->
|
|
19
|
+
<span
|
|
20
|
+
:class="[
|
|
21
|
+
'relative flex items-center justify-center rounded-full border transition-colors duration-150',
|
|
22
|
+
sizeOuter,
|
|
23
|
+
isChecked ? checkedOuter : uncheckedOuter,
|
|
24
|
+
focusClass
|
|
25
|
+
]"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
>
|
|
28
|
+
<!-- inner dot -->
|
|
29
|
+
<span
|
|
30
|
+
v-if="isChecked"
|
|
31
|
+
:class="['rounded-full bg-white transition-transform', sizeInner]"
|
|
32
|
+
/>
|
|
33
|
+
</span>
|
|
34
|
+
|
|
35
|
+
<!-- Label -->
|
|
36
|
+
<span v-if="$slots.default || label" class="select-none ml-2 text-sm" :class="labelClass">
|
|
37
|
+
<slot>{{ selectedlabel }}</slot>
|
|
38
|
+
</span>
|
|
39
|
+
</label>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed, ref, inject, useAttrs } from 'vue'
|
|
44
|
+
import { useLanguageSelected } from '../../../pgo-components/lib/componentConfig'
|
|
45
|
+
|
|
46
|
+
const { language } = inject<{ language: { value: string } }>('i18n') || { language: { value: 'en' } }
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<{
|
|
49
|
+
modelValue?: string|number|boolean|null,
|
|
50
|
+
value: string|number|boolean,
|
|
51
|
+
name?: string,
|
|
52
|
+
label?: string|object,
|
|
53
|
+
disabled?: boolean,
|
|
54
|
+
size?: 'sm'|'md'|'lg',
|
|
55
|
+
lang?: string,
|
|
56
|
+
dir?: string,
|
|
57
|
+
color?: string
|
|
58
|
+
}>(), {
|
|
59
|
+
size: 'md',
|
|
60
|
+
disabled: false,
|
|
61
|
+
color: 'primary'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const emit = defineEmits(['update:modelValue', 'change'])
|
|
65
|
+
|
|
66
|
+
const attrs = useAttrs()
|
|
67
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
68
|
+
|
|
69
|
+
const isChecked = computed(() => {
|
|
70
|
+
// Loose equality to support number/string matching
|
|
71
|
+
// but keep it predictable; change if strict equality preferred
|
|
72
|
+
// (e.g. Number/ String conversion)
|
|
73
|
+
// eslint-disable-next-line eqeqeq
|
|
74
|
+
return props.modelValue == props.value
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const selectedDirection = computed(() => {
|
|
78
|
+
let selected = ''
|
|
79
|
+
if (props.dir == 'rtl') selected = 'rtl'
|
|
80
|
+
else if (props.lang == 'dv') selected = 'rtl'
|
|
81
|
+
else if (props.dir == 'ltr') selected = 'ltr'
|
|
82
|
+
else if (props.lang == 'en') selected = 'ltr'
|
|
83
|
+
else selected = ''
|
|
84
|
+
return selected
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const selectLanguage = computed(() => {
|
|
88
|
+
let Selected = ''
|
|
89
|
+
if (props.lang != ''){
|
|
90
|
+
Selected = props.lang
|
|
91
|
+
}
|
|
92
|
+
else if (props.dir == 'rtl') {
|
|
93
|
+
Selected = 'dv'
|
|
94
|
+
}
|
|
95
|
+
else if (props.dir == 'ltr') {
|
|
96
|
+
Selected = 'en'
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
Selected = language.value
|
|
100
|
+
}
|
|
101
|
+
console.log('selectLanguage in Card222:', Selected)
|
|
102
|
+
return (Selected === 'dv') ? 'dv' : 'en'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const faruma = computed(() => {
|
|
106
|
+
if (selectLanguage.value == 'dv') return 'faruma'
|
|
107
|
+
return ''
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const selectedlabel = computed(() => {
|
|
111
|
+
return useLanguageSelected(props.label, selectLanguage.value, '')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const onChange = (e?: Event) => {
|
|
115
|
+
if (props.disabled) return
|
|
116
|
+
emit('update:modelValue', props.value)
|
|
117
|
+
emit('change', props.value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Size classes
|
|
121
|
+
const sizeOuter = computed(() => {
|
|
122
|
+
switch (props.size) {
|
|
123
|
+
case 'sm': return 'w-4 h-4'
|
|
124
|
+
case 'lg': return 'w-6 h-6'
|
|
125
|
+
default: return 'w-5 h-5'
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
const sizeInner = computed(() => {
|
|
129
|
+
switch (props.size) {
|
|
130
|
+
case 'sm': return 'w-2 h-2'
|
|
131
|
+
case 'lg': return 'w-3 h-3'
|
|
132
|
+
default: return 'w-2.5 h-2.5'
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Color mapping (common tokens in your project). Accept a Tailwind class string too.
|
|
137
|
+
const colorClass = computed(() => {
|
|
138
|
+
const c = String(props.color || 'primary')
|
|
139
|
+
// If user passed a tailwind color class like "bg-red-500" or "text-red-500", use it.
|
|
140
|
+
if (/^(bg-|text-|border-)/.test(c)) return c
|
|
141
|
+
// Map simple tokens to classes — adapt to your project's tokens
|
|
142
|
+
const map: Record<string, string> = {
|
|
143
|
+
primary: 'bg-primary',
|
|
144
|
+
secondary: 'bg-secondary',
|
|
145
|
+
success: 'bg-green-500',
|
|
146
|
+
danger: 'bg-red-500',
|
|
147
|
+
info: 'bg-sky-500',
|
|
148
|
+
warning: 'bg-amber-500'
|
|
149
|
+
}
|
|
150
|
+
return map[c] || 'bg-primary'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Checked / unchecked classes
|
|
154
|
+
const checkedOuter = computed(() => `${colorClass.value} border-transparent`)
|
|
155
|
+
const uncheckedOuter = computed(() => 'bg-white border-gray-300 dark:border-gray-600')
|
|
156
|
+
|
|
157
|
+
// Focus ring
|
|
158
|
+
const focusClass = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary'
|
|
159
|
+
|
|
160
|
+
// Optional label color
|
|
161
|
+
const labelClass = computed(() => props.disabled ? 'text-gray-400' : 'text-text')
|
|
162
|
+
</script>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:id="id"
|
|
4
|
+
role="radiogroup"
|
|
5
|
+
:aria-labelledby="ariaLabelledby"
|
|
6
|
+
:aria-disabled="disabled ? 'true' : undefined"
|
|
7
|
+
:class="['inline-flex', direction === 'vertical' ? 'flex-col' : 'items-center', gapClass]"
|
|
8
|
+
@keydown="onKeydown"
|
|
9
|
+
>
|
|
10
|
+
<!-- Render from options prop if provided -->
|
|
11
|
+
<template v-if="Array.isArray(options) && options.length > 0">
|
|
12
|
+
<label
|
|
13
|
+
v-for="(opt, idx) in options"
|
|
14
|
+
:key="`opt-${idx}-${String(opt?.value)}`"
|
|
15
|
+
:class="['inline-flex items-center', { 'opacity-50 pointer-events-none': disabled || opt.disabled }]"
|
|
16
|
+
>
|
|
17
|
+
<Radio
|
|
18
|
+
ref="setOptionRef(idx)"
|
|
19
|
+
v-model="internalValue"
|
|
20
|
+
:value="opt.value"
|
|
21
|
+
:name="groupName"
|
|
22
|
+
:disabled="disabled || !!opt.disabled"
|
|
23
|
+
:size="size"
|
|
24
|
+
:color="opt.color || color"
|
|
25
|
+
@change="onChange(opt.value)"
|
|
26
|
+
>
|
|
27
|
+
<template #default>
|
|
28
|
+
<div class="flex flex-col">
|
|
29
|
+
<div class="select-none ml-2 text-sm" :class="labelClass">{{ opt.label }}</div>
|
|
30
|
+
<small v-if="opt.description" class="text-xs text-gray-500 ml-2">{{ opt.description }}</small>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
</Radio>
|
|
34
|
+
</label>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<!-- Fallback: render default slot - expected to contain Radio children -->
|
|
38
|
+
<slot v-else />
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
44
|
+
import Radio from './Radio.vue'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
Props
|
|
48
|
+
- modelValue: the v-model value
|
|
49
|
+
- options: optional array of { label, value, disabled?, description?, color? }
|
|
50
|
+
- name: optional group name (if not provided a unique name is generated)
|
|
51
|
+
- disabled, size, color, direction
|
|
52
|
+
- ariaLabelledby: optional id for label element (for accessibility)
|
|
53
|
+
*/
|
|
54
|
+
const props = defineProps<{
|
|
55
|
+
modelValue?: unknown,
|
|
56
|
+
options?: Array<{
|
|
57
|
+
label: string,
|
|
58
|
+
value: any,
|
|
59
|
+
disabled?: boolean,
|
|
60
|
+
description?: string,
|
|
61
|
+
color?: string
|
|
62
|
+
}>,
|
|
63
|
+
name?: string,
|
|
64
|
+
disabled?: boolean,
|
|
65
|
+
size?: 'sm'|'md'|'lg',
|
|
66
|
+
color?: string,
|
|
67
|
+
direction?: 'horizontal'|'vertical',
|
|
68
|
+
gap?: string,
|
|
69
|
+
ariaLabelledby?: string
|
|
70
|
+
}>()
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits<{
|
|
73
|
+
(e: 'update:modelValue', value: any): void
|
|
74
|
+
(e: 'change', value: any): void
|
|
75
|
+
}>()
|
|
76
|
+
|
|
77
|
+
// local id + group name
|
|
78
|
+
const uid = Math.random().toString(36).slice(2, 9)
|
|
79
|
+
const id = `radio-group-${uid}`
|
|
80
|
+
const groupName = computed(() => props.name || `rg-${uid}`)
|
|
81
|
+
const ariaLabelledby = computed(() => props.ariaLabelledby || undefined)
|
|
82
|
+
|
|
83
|
+
// layout helpers
|
|
84
|
+
const direction = computed(() => props.direction || 'horizontal')
|
|
85
|
+
const gapClass = computed(() => {
|
|
86
|
+
if (props.gap) return props.gap
|
|
87
|
+
return direction.value === 'vertical' ? 'space-y-2' : 'space-x-4'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// simple defaults
|
|
91
|
+
const size = computed(() => props.size || 'md')
|
|
92
|
+
const color = computed(() => props.color || 'primary')
|
|
93
|
+
const disabled = computed(() => !!props.disabled)
|
|
94
|
+
const labelClass = computed(() => disabled.value ? 'text-gray-400' : 'text-text')
|
|
95
|
+
|
|
96
|
+
// internal v-model proxy
|
|
97
|
+
const internalValue = ref(props.modelValue)
|
|
98
|
+
watch(() => props.modelValue, v => { internalValue.value = v }, { immediate: true })
|
|
99
|
+
watch(internalValue, (val) => {
|
|
100
|
+
emit('update:modelValue', val)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// refs to child radios for keyboard navigation
|
|
104
|
+
const optionRefs = ref<Array<HTMLElement | null>>([])
|
|
105
|
+
|
|
106
|
+
function setOptionRef(index: number) {
|
|
107
|
+
return (el: HTMLElement | null) => {
|
|
108
|
+
optionRefs.value[index] = el
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// update:modelValue already emitted via watcher above; this emits a change event too
|
|
113
|
+
function onChange(val: any) {
|
|
114
|
+
return () => {
|
|
115
|
+
emit('change', val)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Keyboard navigation: ArrowRight/ArrowDown => next, ArrowLeft/ArrowUp => prev, Home/End
|
|
120
|
+
function focusIndex(index: number) {
|
|
121
|
+
const el = optionRefs.value[index]
|
|
122
|
+
if (el && typeof (el as any).focus === 'function') {
|
|
123
|
+
;(el as any).focus()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findEnabledIndex(start = 0, step = 1) {
|
|
128
|
+
const len = optionRefs.value.length
|
|
129
|
+
if (len === 0) return -1
|
|
130
|
+
let i = (start + len) % len
|
|
131
|
+
for (let iter = 0; iter < len; iter++) {
|
|
132
|
+
const idx = (i + iter * step + len) % len
|
|
133
|
+
const el = optionRefs.value[idx]
|
|
134
|
+
if (!el) continue
|
|
135
|
+
const radioEl = el.querySelector ? el.querySelector('input[type="radio"]') : el
|
|
136
|
+
if (!radioEl) continue
|
|
137
|
+
if ((radioEl as HTMLInputElement).disabled) continue
|
|
138
|
+
return idx
|
|
139
|
+
}
|
|
140
|
+
return -1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function onKeydown(e: KeyboardEvent) {
|
|
144
|
+
if (!Array.isArray(props.options) || props.options.length === 0) return
|
|
145
|
+
const len = props.options.length
|
|
146
|
+
if (len === 0) return
|
|
147
|
+
|
|
148
|
+
const currentIndex = props.options.findIndex(o => o.value == internalValue.value)
|
|
149
|
+
|
|
150
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
151
|
+
e.preventDefault()
|
|
152
|
+
const next = findEnabledIndex((currentIndex >= 0 ? currentIndex : -1) + 1, 1)
|
|
153
|
+
if (next >= 0) {
|
|
154
|
+
internalValue.value = props.options[next].value
|
|
155
|
+
await nextTick()
|
|
156
|
+
focusIndex(next)
|
|
157
|
+
emit('change', internalValue.value)
|
|
158
|
+
}
|
|
159
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
160
|
+
e.preventDefault()
|
|
161
|
+
const prev = findEnabledIndex((currentIndex >= 0 ? currentIndex : len) - 1, -1)
|
|
162
|
+
if (prev >= 0) {
|
|
163
|
+
internalValue.value = props.options[prev].value
|
|
164
|
+
await nextTick()
|
|
165
|
+
focusIndex(prev)
|
|
166
|
+
emit('change', internalValue.value)
|
|
167
|
+
}
|
|
168
|
+
} else if (e.key === 'Home') {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
const first = findEnabledIndex(0, 1)
|
|
171
|
+
if (first >= 0) {
|
|
172
|
+
internalValue.value = props.options[first].value
|
|
173
|
+
await nextTick()
|
|
174
|
+
focusIndex(first)
|
|
175
|
+
emit('change', internalValue.value)
|
|
176
|
+
}
|
|
177
|
+
} else if (e.key === 'End') {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
const last = findEnabledIndex(len - 1, -1)
|
|
180
|
+
if (last >= 0) {
|
|
181
|
+
internalValue.value = props.options[last].value
|
|
182
|
+
await nextTick()
|
|
183
|
+
focusIndex(last)
|
|
184
|
+
emit('change', internalValue.value)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</script>
|