orio-ui 0.1.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/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +3 -0
- package/dist/module.d.ts +3 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +16 -0
- package/dist/runtime/assets/css/animation.css +1 -0
- package/dist/runtime/assets/css/colors.css +1 -0
- package/dist/runtime/assets/css/cool-gradient-hover.css +23 -0
- package/dist/runtime/assets/css/main.css +1 -0
- package/dist/runtime/assets/css/scroll.css +1 -0
- package/dist/runtime/components/Button.vue +102 -0
- package/dist/runtime/components/CheckBox.vue +93 -0
- package/dist/runtime/components/ControlElement.vue +39 -0
- package/dist/runtime/components/DashedContainer.vue +59 -0
- package/dist/runtime/components/DatePicker.vue +30 -0
- package/dist/runtime/components/DateRangePicker.vue +73 -0
- package/dist/runtime/components/EmptyState.vue +81 -0
- package/dist/runtime/components/Icon.vue +40 -0
- package/dist/runtime/components/Input.vue +48 -0
- package/dist/runtime/components/LoadingSpinner.vue +6 -0
- package/dist/runtime/components/Modal.vue +69 -0
- package/dist/runtime/components/Popover.vue +249 -0
- package/dist/runtime/components/Selector.vue +208 -0
- package/dist/runtime/components/Tag.vue +21 -0
- package/dist/runtime/components/Textarea.vue +53 -0
- package/dist/runtime/components/view/Dates.vue +59 -0
- package/dist/runtime/components/view/Separator.vue +26 -0
- package/dist/runtime/components/view/Text.vue +79 -0
- package/dist/runtime/composables/index.d.ts +4 -0
- package/dist/runtime/composables/index.js +4 -0
- package/dist/runtime/composables/useApi.d.ts +10 -0
- package/dist/runtime/composables/useApi.js +9 -0
- package/dist/runtime/composables/useFuzzySearch.d.ts +10 -0
- package/dist/runtime/composables/useFuzzySearch.js +22 -0
- package/dist/runtime/composables/useModal.d.ts +15 -0
- package/dist/runtime/composables/useModal.js +28 -0
- package/dist/runtime/composables/useTheme.d.ts +6 -0
- package/dist/runtime/composables/useTheme.js +23 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +20 -0
- package/dist/runtime/utils/icon-registry.d.ts +2 -0
- package/dist/runtime/utils/icon-registry.js +26 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/nuxt.config.ts +38 -0
- package/package.json +99 -0
- package/src/module.ts +16 -0
- package/src/runtime/assets/css/animation.css +88 -0
- package/src/runtime/assets/css/colors.css +142 -0
- package/src/runtime/assets/css/cool-gradient-hover.scss +33 -0
- package/src/runtime/assets/css/main.css +11 -0
- package/src/runtime/assets/css/scroll.css +46 -0
- package/src/runtime/components/Button.vue +110 -0
- package/src/runtime/components/CheckBox.vue +103 -0
- package/src/runtime/components/ControlElement.vue +42 -0
- package/src/runtime/components/DashedContainer.vue +60 -0
- package/src/runtime/components/DatePicker.vue +84 -0
- package/src/runtime/components/DateRangePicker.vue +74 -0
- package/src/runtime/components/EmptyState.vue +87 -0
- package/src/runtime/components/Icon.vue +51 -0
- package/src/runtime/components/Input.vue +54 -0
- package/src/runtime/components/LoadingSpinner.vue +6 -0
- package/src/runtime/components/Modal.vue +111 -0
- package/src/runtime/components/Popover.vue +249 -0
- package/src/runtime/components/Selector.vue +224 -0
- package/src/runtime/components/Tag.vue +45 -0
- package/src/runtime/components/Textarea.vue +59 -0
- package/src/runtime/components/view/Dates.vue +61 -0
- package/src/runtime/components/view/Separator.vue +30 -0
- package/src/runtime/components/view/Text.vue +83 -0
- package/src/runtime/composables/index.ts +4 -0
- package/src/runtime/composables/useApi.ts +26 -0
- package/src/runtime/composables/useFuzzySearch.ts +51 -0
- package/src/runtime/composables/useModal.ts +47 -0
- package/src/runtime/composables/useTheme.ts +31 -0
- package/src/runtime/index.ts +25 -0
- package/src/runtime/utils/icon-registry.ts +41 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div ref="trigger" @click="togglePopover()">
|
|
4
|
+
<slot :toggle="togglePopover" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<Teleport to="body">
|
|
8
|
+
<Transition name="animate-fade-slide" appear>
|
|
9
|
+
<div
|
|
10
|
+
v-if="showPopover"
|
|
11
|
+
ref="popover"
|
|
12
|
+
class="popover"
|
|
13
|
+
:style="popoverStyle"
|
|
14
|
+
>
|
|
15
|
+
<slot name="content" :toggle="togglePopover" />
|
|
16
|
+
</div>
|
|
17
|
+
</Transition>
|
|
18
|
+
</Teleport>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { ref, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
24
|
+
import { useElementBounding } from '@vueuse/core';
|
|
25
|
+
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
/**
|
|
28
|
+
* Defines where the popover is placed relative to the trigger.
|
|
29
|
+
* Acceptable single values: 'top', 'bottom', 'left', 'right'
|
|
30
|
+
* Acceptable combos: 'top-left', 'top-right', 'bottom-left', 'bottom-right',
|
|
31
|
+
* 'left-top', 'left-bottom', 'right-top', 'right-bottom'
|
|
32
|
+
* If you only provide 'top', 'bottom', 'left', or 'right',
|
|
33
|
+
* it aligns center by default.
|
|
34
|
+
*/
|
|
35
|
+
position: {
|
|
36
|
+
type: String,
|
|
37
|
+
default: 'bottom-left',
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* Distance (in px) between the popover and the trigger element.
|
|
41
|
+
*/
|
|
42
|
+
offset: {
|
|
43
|
+
type: Number,
|
|
44
|
+
default: 10,
|
|
45
|
+
},
|
|
46
|
+
disabled: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const trigger = ref(null);
|
|
53
|
+
const popover = ref(null);
|
|
54
|
+
|
|
55
|
+
const { width: popoverWidth, height: popoverHeight } =
|
|
56
|
+
useElementBounding(popover);
|
|
57
|
+
|
|
58
|
+
const showPopover = ref(false);
|
|
59
|
+
const triggerRect = ref(null);
|
|
60
|
+
const popoverRect = ref(null);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculates the inline style for the popover based on position & offset.
|
|
64
|
+
*/
|
|
65
|
+
const popoverStyle = computed(() => {
|
|
66
|
+
if (!showPopover.value || !triggerRect.value || !popoverRect.value) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [main, sub = 'center'] = currentPosition.value.split('-');
|
|
71
|
+
const offset = props.offset;
|
|
72
|
+
|
|
73
|
+
const tRect = triggerRect.value;
|
|
74
|
+
const pRect = popoverRect.value;
|
|
75
|
+
|
|
76
|
+
let topValue = 0;
|
|
77
|
+
let leftValue = 0;
|
|
78
|
+
|
|
79
|
+
// Calculate vertical position (top)
|
|
80
|
+
if (main === 'top') {
|
|
81
|
+
topValue = tRect.top - offset - pRect.height;
|
|
82
|
+
} else if (main === 'bottom') {
|
|
83
|
+
topValue = tRect.bottom + offset;
|
|
84
|
+
} else {
|
|
85
|
+
// For 'left' or 'right' main, center vertically
|
|
86
|
+
topValue = tRect.top + tRect.height / 2 - pRect.height / 2;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Calculate horizontal position (left)
|
|
90
|
+
if (sub === 'left') {
|
|
91
|
+
leftValue = tRect.right - pRect.width;
|
|
92
|
+
} else if (sub === 'right') {
|
|
93
|
+
leftValue = tRect.left;
|
|
94
|
+
} else {
|
|
95
|
+
// 'center' is default horizontally
|
|
96
|
+
leftValue = tRect.left + tRect.width / 2 - pRect.width / 2;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If the main position is 'left' or 'right', override horizontal positioning
|
|
100
|
+
if (main === 'left') {
|
|
101
|
+
leftValue = tRect.left - offset - pRect.width;
|
|
102
|
+
} else if (main === 'right') {
|
|
103
|
+
leftValue = tRect.right + offset;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
top: `${topValue}px`,
|
|
108
|
+
left: `${leftValue}px`,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const currentPosition = ref(props.position);
|
|
113
|
+
|
|
114
|
+
const getFallbackPositions = (pos: string) => {
|
|
115
|
+
const [main, sub = 'center'] = pos.split('-');
|
|
116
|
+
|
|
117
|
+
const opposites: Record<string, string> = {
|
|
118
|
+
top: 'bottom',
|
|
119
|
+
bottom: 'top',
|
|
120
|
+
left: 'right',
|
|
121
|
+
right: 'left',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const allPositions = [
|
|
125
|
+
`${main}-${sub}`,
|
|
126
|
+
`${opposites[main]}-${sub}`,
|
|
127
|
+
`${main}-center`,
|
|
128
|
+
`${opposites[main]}-center`,
|
|
129
|
+
`${sub}-${main}`, // e.g. left-top
|
|
130
|
+
`${sub}-${opposites[main]}`,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
return [...new Set(allPositions)];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function checkIfFits(position: string, tRect: any, pRect: any, offset: number) {
|
|
137
|
+
const [main, sub = 'center'] = position.split('-');
|
|
138
|
+
|
|
139
|
+
const space = {
|
|
140
|
+
top: tRect.top,
|
|
141
|
+
bottom: window.innerHeight - tRect.bottom,
|
|
142
|
+
left: tRect.left,
|
|
143
|
+
right: window.innerWidth - tRect.right,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (main === 'top' && space.top < pRect.height + offset) return false;
|
|
147
|
+
if (main === 'bottom' && space.bottom < pRect.height + offset) return false;
|
|
148
|
+
if (main === 'left' && space.left < pRect.width + offset) return false;
|
|
149
|
+
if (main === 'right' && space.right < pRect.width + offset) return false;
|
|
150
|
+
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Calculates bounding client rects for trigger & popover,
|
|
156
|
+
* updating reactive refs.
|
|
157
|
+
*/
|
|
158
|
+
async function updateRects() {
|
|
159
|
+
await nextTick();
|
|
160
|
+
const triggerEl = trigger.value;
|
|
161
|
+
const popoverEl = popover.value?.firstElementChild || popover.value;
|
|
162
|
+
|
|
163
|
+
if (!triggerEl || !popoverEl) return;
|
|
164
|
+
|
|
165
|
+
const tRect = triggerEl.getBoundingClientRect();
|
|
166
|
+
triggerRect.value = tRect;
|
|
167
|
+
|
|
168
|
+
const fallbacks = getFallbackPositions(props.position);
|
|
169
|
+
|
|
170
|
+
for (const pos of fallbacks) {
|
|
171
|
+
// temporarily apply style to measure it
|
|
172
|
+
popoverEl.style.visibility = 'hidden';
|
|
173
|
+
popoverEl.style.display = 'block';
|
|
174
|
+
|
|
175
|
+
const pRect = popoverEl.getBoundingClientRect();
|
|
176
|
+
const fits = checkIfFits(pos, tRect, pRect, props.offset);
|
|
177
|
+
|
|
178
|
+
if (fits) {
|
|
179
|
+
popoverRect.value = pRect;
|
|
180
|
+
currentPosition.value = pos;
|
|
181
|
+
popoverEl.style.visibility = '';
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// fallback to original
|
|
187
|
+
popoverRect.value = popoverEl.getBoundingClientRect();
|
|
188
|
+
currentPosition.value = props.position;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Toggles popover visibility.
|
|
193
|
+
* @param {boolean|null} force - If `true`/`false`, force that state; if `null`, toggle.
|
|
194
|
+
*/
|
|
195
|
+
async function togglePopover(force: boolean | null = null) {
|
|
196
|
+
if (props.disabled) return;
|
|
197
|
+
showPopover.value = force !== null ? force : !showPopover.value;
|
|
198
|
+
if (!showPopover.value) return;
|
|
199
|
+
|
|
200
|
+
await nextTick();
|
|
201
|
+
updateRects();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Closes the popover if the click is outside trigger/popover.
|
|
206
|
+
*/
|
|
207
|
+
function handleDocumentClick(e: MouseEvent) {
|
|
208
|
+
if (!showPopover.value) return;
|
|
209
|
+
|
|
210
|
+
const clickedInsideTrigger =
|
|
211
|
+
trigger.value && trigger.value.contains(e.target as Node);
|
|
212
|
+
const clickedInsidePopover =
|
|
213
|
+
popover.value && popover.value.contains(e.target as Node);
|
|
214
|
+
|
|
215
|
+
if (!clickedInsideTrigger && !clickedInsidePopover) {
|
|
216
|
+
showPopover.value = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Updates position of popover on scroll/resize if popover is open.
|
|
222
|
+
*/
|
|
223
|
+
function redrawPopover() {
|
|
224
|
+
if (!showPopover.value) return;
|
|
225
|
+
updateRects();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
watch([popoverWidth, popoverHeight], redrawPopover);
|
|
229
|
+
|
|
230
|
+
onMounted(() => {
|
|
231
|
+
document.addEventListener('click', handleDocumentClick);
|
|
232
|
+
window.addEventListener('scroll', redrawPopover, true);
|
|
233
|
+
window.addEventListener('resize', redrawPopover, true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
onBeforeUnmount(() => {
|
|
237
|
+
document.removeEventListener('click', handleDocumentClick);
|
|
238
|
+
window.removeEventListener('scroll', redrawPopover, true);
|
|
239
|
+
window.removeEventListener('resize', redrawPopover, true);
|
|
240
|
+
});
|
|
241
|
+
</script>
|
|
242
|
+
<style lang="scss" scoped>
|
|
243
|
+
.popover {
|
|
244
|
+
border: 0;
|
|
245
|
+
background-color: transparent;
|
|
246
|
+
position: fixed;
|
|
247
|
+
z-index: 1;
|
|
248
|
+
}
|
|
249
|
+
</style>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends object">
|
|
2
|
+
import { computed, toRefs } from 'vue';
|
|
3
|
+
|
|
4
|
+
export type SelectableOption<T extends object = object> = string | T;
|
|
5
|
+
|
|
6
|
+
export interface SelectProps<T extends object = object> {
|
|
7
|
+
options: SelectableOption[];
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
field?: string;
|
|
10
|
+
optionName?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<SelectProps>(), {
|
|
15
|
+
placeholder: 'Select an option',
|
|
16
|
+
field: 'id',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const { field, optionName, placeholder } = toRefs(props);
|
|
20
|
+
|
|
21
|
+
const modelValue = defineModel<SelectableOption | SelectableOption[] | null | undefined>({
|
|
22
|
+
required: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Key to the object when option is not 'string'
|
|
26
|
+
const key = computed(() => field.value as Extract<keyof T, string>);
|
|
27
|
+
const label = computed(() => optionName.value as Extract<keyof T, string>);
|
|
28
|
+
|
|
29
|
+
const flatModalValue = computed(() => {
|
|
30
|
+
if (!modelValue.value) return null;
|
|
31
|
+
if (!props.multiple || !Array.isArray(modelValue.value))
|
|
32
|
+
return typeof modelValue.value === 'string'
|
|
33
|
+
? modelValue.value
|
|
34
|
+
: (modelValue.value as T)[key.value];
|
|
35
|
+
return modelValue.value.map((option) =>
|
|
36
|
+
typeof option === 'string' ? option : (option as T)[key.value]
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function toggleOption(option: SelectableOption, toggle: () => void) {
|
|
41
|
+
if (props.multiple) {
|
|
42
|
+
if (Array.isArray(modelValue.value)) {
|
|
43
|
+
const index = modelValue.value.findIndex((opt) =>
|
|
44
|
+
typeof option === 'string' ? option === opt : opt[key.value] === (option as T)[key.value]
|
|
45
|
+
);
|
|
46
|
+
if (index > -1) {
|
|
47
|
+
modelValue.value.splice(index, 1);
|
|
48
|
+
} else {
|
|
49
|
+
modelValue.value.push(option);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
modelValue.value = [option];
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
modelValue.value = option;
|
|
56
|
+
// should later be turned off with some additional interaction modes
|
|
57
|
+
toggle();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isOptionSelected(option: SelectableOption): boolean {
|
|
62
|
+
if (typeof option === 'string') return modelValue.value === option;
|
|
63
|
+
return !!(
|
|
64
|
+
flatModalValue.value &&
|
|
65
|
+
(flatModalValue.value === (option as T)[key.value] ||
|
|
66
|
+
(flatModalValue.value as string[]).includes((option as T)[key.value] as string))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getOptionLabel(option: SelectableOption | undefined): string {
|
|
71
|
+
if (!option) return placeholder.value;
|
|
72
|
+
if (typeof option === 'string') return option;
|
|
73
|
+
if (optionName.value) return String((option as T)[label.value]);
|
|
74
|
+
return JSON.stringify(option);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getOptionKey(option: SelectableOption): string | number {
|
|
78
|
+
if (typeof option === 'string') return option;
|
|
79
|
+
return String((option as T)[key.value]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const selectorAttrs = computed(() => ({ getOptionKey, getOptionLabel }));
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<orio-control-element>
|
|
87
|
+
<orio-popover position="bottom-right" :offset="5">
|
|
88
|
+
<template #default="{ toggle }">
|
|
89
|
+
<slot name="trigger" :toggle>
|
|
90
|
+
<div class="selector-trigger">
|
|
91
|
+
<slot name="trigger-content" :toggle v-bind="selectorAttrs" :attrs="$attrs">
|
|
92
|
+
<div class="trigger-content">
|
|
93
|
+
<slot name="trigger-label" :toggle v-bind="selectorAttrs" :attrs="$attrs">
|
|
94
|
+
<template v-if="!props.multiple">
|
|
95
|
+
{{ getOptionLabel(modelValue as T) }}
|
|
96
|
+
</template>
|
|
97
|
+
<template v-else-if="Array.isArray(modelValue)">
|
|
98
|
+
<span> {{ modelValue!.length }} selected </span>
|
|
99
|
+
</template>
|
|
100
|
+
</slot>
|
|
101
|
+
</div>
|
|
102
|
+
<orio-icon name="chevron-down" />
|
|
103
|
+
</slot>
|
|
104
|
+
</div>
|
|
105
|
+
</slot>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<template #content="{ toggle }">
|
|
109
|
+
<div class="selector-content">
|
|
110
|
+
<ul v-if="options.length">
|
|
111
|
+
<li
|
|
112
|
+
v-for="option in options"
|
|
113
|
+
:key="getOptionKey(option)"
|
|
114
|
+
:class="{ selected: isOptionSelected(option) }"
|
|
115
|
+
@click="toggleOption(option, toggle)"
|
|
116
|
+
>
|
|
117
|
+
<slot
|
|
118
|
+
name="option"
|
|
119
|
+
:option
|
|
120
|
+
:toggle
|
|
121
|
+
:selected="isOptionSelected(option)"
|
|
122
|
+
v-bind="selectorAttrs"
|
|
123
|
+
>
|
|
124
|
+
{{ getOptionLabel(option) }}
|
|
125
|
+
</slot>
|
|
126
|
+
</li>
|
|
127
|
+
</ul>
|
|
128
|
+
<slot v-else name="no-options">
|
|
129
|
+
<orio-empty-state title="No options found" size="small" />
|
|
130
|
+
</slot>
|
|
131
|
+
<slot name="options-addon" />
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
</orio-popover>
|
|
135
|
+
</orio-control-element>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<style lang="scss" scoped>
|
|
139
|
+
.selector-trigger {
|
|
140
|
+
z-index: 1;
|
|
141
|
+
min-height: 1.5rem;
|
|
142
|
+
user-select: none;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: space-between;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
|
|
148
|
+
background: var(--color-bg);
|
|
149
|
+
border: 1px solid var(--color-border);
|
|
150
|
+
border-radius: 6px;
|
|
151
|
+
padding: 0.5rem 0.75rem;
|
|
152
|
+
font-size: 0.95rem;
|
|
153
|
+
color: var(--color-text);
|
|
154
|
+
transition:
|
|
155
|
+
border-color 0.2s ease,
|
|
156
|
+
box-shadow 0.2s ease,
|
|
157
|
+
background-color 0.2s ease;
|
|
158
|
+
|
|
159
|
+
&:hover {
|
|
160
|
+
border-color: var(--color-accent);
|
|
161
|
+
background-color: var(--color-surface); /* subtle lift */
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&:focus-within {
|
|
165
|
+
border-color: var(--color-accent);
|
|
166
|
+
box-shadow: 0 0 0 2px var(--color-surface);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.icon {
|
|
170
|
+
color: var(--color-muted);
|
|
171
|
+
transition: color 0.2s ease;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&:hover .icon {
|
|
175
|
+
color: var(--color-accent);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.selector-content {
|
|
180
|
+
min-width: 15rem;
|
|
181
|
+
max-height: 20rem;
|
|
182
|
+
overflow: auto;
|
|
183
|
+
|
|
184
|
+
background: var(--color-bg);
|
|
185
|
+
border: 1px solid var(--color-border);
|
|
186
|
+
border-radius: 6px;
|
|
187
|
+
margin-top: 0.25rem;
|
|
188
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
189
|
+
|
|
190
|
+
ul {
|
|
191
|
+
list-style: none;
|
|
192
|
+
padding: 0;
|
|
193
|
+
margin: 0;
|
|
194
|
+
|
|
195
|
+
li {
|
|
196
|
+
padding: 0.5rem 0.75rem;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition:
|
|
199
|
+
background-color 0.15s ease,
|
|
200
|
+
color 0.15s ease;
|
|
201
|
+
|
|
202
|
+
color: var(--color-text);
|
|
203
|
+
|
|
204
|
+
&:hover {
|
|
205
|
+
background-color: var(--color-surface); /* neutral lift */
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
&.selected {
|
|
209
|
+
background-color: var(--color-accent);
|
|
210
|
+
color: var(--color-accent-soft);
|
|
211
|
+
font-weight: 500;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.trigger-content {
|
|
218
|
+
width: 100%;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
:deep(.popover) {
|
|
222
|
+
width: 100%;
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export type TagStyle = 'neutral' | 'accent';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
text: string;
|
|
6
|
+
variant?: TagStyle;
|
|
7
|
+
}
|
|
8
|
+
withDefaults(defineProps<Props>(), {
|
|
9
|
+
variant: 'neutral',
|
|
10
|
+
});
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span class="tag" :class="`tag--${variant}`">
|
|
15
|
+
{{ text }}
|
|
16
|
+
</span>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<style scoped>
|
|
20
|
+
.tag {
|
|
21
|
+
max-height: 1rem;
|
|
22
|
+
display: inline-block;
|
|
23
|
+
padding: 0.25rem 0.6rem;
|
|
24
|
+
border-radius: 1rem;
|
|
25
|
+
font-size: 0.8rem;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
line-height: 1;
|
|
28
|
+
border: 1px solid transparent;
|
|
29
|
+
user-select: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Neutral (gray chip) */
|
|
33
|
+
.tag--neutral {
|
|
34
|
+
background-color: var(--color-surface);
|
|
35
|
+
border-color: color-mix(in srgb, var(--color-border) 80%, var(--color-accent) 20%);
|
|
36
|
+
color: var(--color-muted);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Accent (themed chip) */
|
|
40
|
+
.tag--accent {
|
|
41
|
+
background-color: var(--color-accent-soft);
|
|
42
|
+
border-color: var(--color-accent-border);
|
|
43
|
+
color: var(--color-accent);
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAttrs } from 'vue';
|
|
3
|
+
|
|
4
|
+
const attrs = useAttrs();
|
|
5
|
+
defineEmits<{
|
|
6
|
+
(e: 'input', value: string): void;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const modelValue = defineModel<string>({ default: '' });
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<orio-control-element v-bind="attrs">
|
|
14
|
+
<textarea
|
|
15
|
+
v-bind="attrs"
|
|
16
|
+
v-model="modelValue"
|
|
17
|
+
rows="4"
|
|
18
|
+
class="textarea"
|
|
19
|
+
/>
|
|
20
|
+
</orio-control-element>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<style lang="scss" scoped>
|
|
24
|
+
.textarea {
|
|
25
|
+
width: 100%;
|
|
26
|
+
padding: 0.5rem 0.75rem;
|
|
27
|
+
border: 1px solid var(--color-border);
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
font-size: 1rem;
|
|
30
|
+
line-height: 1.4;
|
|
31
|
+
color: var(--color-text);
|
|
32
|
+
background-color: var(--color-bg);
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
resize: vertical; /* Let user resize vertically only */
|
|
35
|
+
transition:
|
|
36
|
+
border-color 0.2s ease,
|
|
37
|
+
box-shadow 0.2s ease;
|
|
38
|
+
|
|
39
|
+
&::placeholder {
|
|
40
|
+
color: var(--color-muted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&:hover {
|
|
44
|
+
border-color: var(--color-accent);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&:focus {
|
|
48
|
+
border-color: var(--color-accent);
|
|
49
|
+
box-shadow: 0 0 0 2px var(--color-accent-soft);
|
|
50
|
+
outline: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:disabled {
|
|
54
|
+
background-color: var(--color-surface);
|
|
55
|
+
color: var(--color-muted);
|
|
56
|
+
cursor: not-allowed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, toRefs } from 'vue';
|
|
3
|
+
|
|
4
|
+
export interface ResumeDate {
|
|
5
|
+
startDate: string;
|
|
6
|
+
endDate?: string | null; // undefined - mean single date, null - means "Present"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
dates: ResumeDate;
|
|
11
|
+
month?: boolean; // Optional prop to indicate if the date should be displayed as month/year
|
|
12
|
+
size?: 'small' | 'medium' | 'large';
|
|
13
|
+
type?: 'text' | 'title' | 'subtitle' | 'italics';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
size: 'small',
|
|
18
|
+
type: 'italics',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { dates } = toRefs(props);
|
|
22
|
+
|
|
23
|
+
function formatMonthYear(value: string) {
|
|
24
|
+
if (!value) return '';
|
|
25
|
+
if (!props.month)
|
|
26
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
27
|
+
day: 'numeric',
|
|
28
|
+
month: 'short',
|
|
29
|
+
year: 'numeric',
|
|
30
|
+
}).format(new Date(value));
|
|
31
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
32
|
+
month: 'short',
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
}).format(new Date(value));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const startDate = computed(() => formatMonthYear(dates.value.startDate));
|
|
38
|
+
|
|
39
|
+
const endDate = computed(() => {
|
|
40
|
+
if (dates.value.endDate === undefined) return null;
|
|
41
|
+
return dates.value.endDate !== null
|
|
42
|
+
? formatMonthYear(dates.value.endDate)
|
|
43
|
+
: 'Present';
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<div class="view-date">
|
|
49
|
+
<orio-view-text :model-value="startDate" :type :size />
|
|
50
|
+
<template v-if="endDate">
|
|
51
|
+
<span v-if="startDate"> - </span>
|
|
52
|
+
<orio-view-text :model-value="endDate" :type :size />
|
|
53
|
+
</template>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<style scoped>
|
|
58
|
+
.view-date * {
|
|
59
|
+
display: inline;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
style?: 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge';
|
|
6
|
+
size?: number | string;
|
|
7
|
+
margin?: number | string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
11
|
+
style: 'solid',
|
|
12
|
+
size: 1,
|
|
13
|
+
margin: 1,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const sizePx = computed(() => `${props.size}px`);
|
|
17
|
+
const margin = computed(() => `${props.margin}rem`);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div />
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<style scoped>
|
|
25
|
+
div {
|
|
26
|
+
width: 100%;
|
|
27
|
+
border-block-end: v-bind(sizePx) v-bind(style) var(--color-border);
|
|
28
|
+
margin-block: v-bind(margin);
|
|
29
|
+
}
|
|
30
|
+
</style>
|