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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -0
  3. package/dist/module.cjs +5 -0
  4. package/dist/module.d.mts +3 -0
  5. package/dist/module.d.ts +3 -0
  6. package/dist/module.json +12 -0
  7. package/dist/module.mjs +16 -0
  8. package/dist/runtime/assets/css/animation.css +1 -0
  9. package/dist/runtime/assets/css/colors.css +1 -0
  10. package/dist/runtime/assets/css/cool-gradient-hover.css +23 -0
  11. package/dist/runtime/assets/css/main.css +1 -0
  12. package/dist/runtime/assets/css/scroll.css +1 -0
  13. package/dist/runtime/components/Button.vue +102 -0
  14. package/dist/runtime/components/CheckBox.vue +93 -0
  15. package/dist/runtime/components/ControlElement.vue +39 -0
  16. package/dist/runtime/components/DashedContainer.vue +59 -0
  17. package/dist/runtime/components/DatePicker.vue +30 -0
  18. package/dist/runtime/components/DateRangePicker.vue +73 -0
  19. package/dist/runtime/components/EmptyState.vue +81 -0
  20. package/dist/runtime/components/Icon.vue +40 -0
  21. package/dist/runtime/components/Input.vue +48 -0
  22. package/dist/runtime/components/LoadingSpinner.vue +6 -0
  23. package/dist/runtime/components/Modal.vue +69 -0
  24. package/dist/runtime/components/Popover.vue +249 -0
  25. package/dist/runtime/components/Selector.vue +208 -0
  26. package/dist/runtime/components/Tag.vue +21 -0
  27. package/dist/runtime/components/Textarea.vue +53 -0
  28. package/dist/runtime/components/view/Dates.vue +59 -0
  29. package/dist/runtime/components/view/Separator.vue +26 -0
  30. package/dist/runtime/components/view/Text.vue +79 -0
  31. package/dist/runtime/composables/index.d.ts +4 -0
  32. package/dist/runtime/composables/index.js +4 -0
  33. package/dist/runtime/composables/useApi.d.ts +10 -0
  34. package/dist/runtime/composables/useApi.js +9 -0
  35. package/dist/runtime/composables/useFuzzySearch.d.ts +10 -0
  36. package/dist/runtime/composables/useFuzzySearch.js +22 -0
  37. package/dist/runtime/composables/useModal.d.ts +15 -0
  38. package/dist/runtime/composables/useModal.js +28 -0
  39. package/dist/runtime/composables/useTheme.d.ts +6 -0
  40. package/dist/runtime/composables/useTheme.js +23 -0
  41. package/dist/runtime/index.d.ts +20 -0
  42. package/dist/runtime/index.js +20 -0
  43. package/dist/runtime/utils/icon-registry.d.ts +2 -0
  44. package/dist/runtime/utils/icon-registry.js +26 -0
  45. package/dist/types.d.mts +7 -0
  46. package/dist/types.d.ts +7 -0
  47. package/nuxt.config.ts +38 -0
  48. package/package.json +99 -0
  49. package/src/module.ts +16 -0
  50. package/src/runtime/assets/css/animation.css +88 -0
  51. package/src/runtime/assets/css/colors.css +142 -0
  52. package/src/runtime/assets/css/cool-gradient-hover.scss +33 -0
  53. package/src/runtime/assets/css/main.css +11 -0
  54. package/src/runtime/assets/css/scroll.css +46 -0
  55. package/src/runtime/components/Button.vue +110 -0
  56. package/src/runtime/components/CheckBox.vue +103 -0
  57. package/src/runtime/components/ControlElement.vue +42 -0
  58. package/src/runtime/components/DashedContainer.vue +60 -0
  59. package/src/runtime/components/DatePicker.vue +84 -0
  60. package/src/runtime/components/DateRangePicker.vue +74 -0
  61. package/src/runtime/components/EmptyState.vue +87 -0
  62. package/src/runtime/components/Icon.vue +51 -0
  63. package/src/runtime/components/Input.vue +54 -0
  64. package/src/runtime/components/LoadingSpinner.vue +6 -0
  65. package/src/runtime/components/Modal.vue +111 -0
  66. package/src/runtime/components/Popover.vue +249 -0
  67. package/src/runtime/components/Selector.vue +224 -0
  68. package/src/runtime/components/Tag.vue +45 -0
  69. package/src/runtime/components/Textarea.vue +59 -0
  70. package/src/runtime/components/view/Dates.vue +61 -0
  71. package/src/runtime/components/view/Separator.vue +30 -0
  72. package/src/runtime/components/view/Text.vue +83 -0
  73. package/src/runtime/composables/index.ts +4 -0
  74. package/src/runtime/composables/useApi.ts +26 -0
  75. package/src/runtime/composables/useFuzzySearch.ts +51 -0
  76. package/src/runtime/composables/useModal.ts +47 -0
  77. package/src/runtime/composables/useTheme.ts +31 -0
  78. package/src/runtime/index.ts +25 -0
  79. package/src/runtime/utils/icon-registry.ts +41 -0
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, computed } from 'vue';
3
+ import type { ResumeDate } from './view/Dates.vue';
4
+
5
+ export interface DateRangePickerProps {
6
+ month?: boolean;
7
+ }
8
+
9
+ defineProps<DateRangePickerProps>();
10
+
11
+ const dates = defineModel<ResumeDate>('dates', { required: true });
12
+
13
+ const present = ref(dates.value.endDate !== '' && !dates.value.endDate);
14
+
15
+ watch(present, (value) => {
16
+ if (value) {
17
+ dates.value.endDate = null; // Set end date to null when present is checked
18
+ } else {
19
+ dates.value.endDate = ''; // Reset end date when present is unchecked
20
+ }
21
+ });
22
+
23
+ const dateIsCorrect = computed(() => {
24
+ // Ensure that the start date is before the end date
25
+ if (dates.value.startDate && dates.value.endDate) {
26
+ return new Date(dates.value.startDate) <= new Date(dates.value.endDate);
27
+ }
28
+ return true; // If one of the dates is empty, consider it correct
29
+ });
30
+
31
+ defineExpose({ dateIsCorrect });
32
+ </script>
33
+
34
+ <template>
35
+ <div>
36
+ <orio-control-element>
37
+ <div class="date-range-picker">
38
+ <orio-date-picker v-model:date="dates.startDate" :month />
39
+ <orio-date-picker v-model:date="dates.endDate" :month />
40
+ <orio-check-box v-model="present"> Present </orio-check-box>
41
+ </div>
42
+ </orio-control-element>
43
+ <div v-if="!dateIsCorrect" class="error-message">
44
+ <p>Start date must be before end date.</p>
45
+ </div>
46
+ </div>
47
+ </template>
48
+
49
+ <style scoped>
50
+ .date-range-picker {
51
+ display: flex;
52
+ align-items: center;
53
+ flex-wrap: wrap;
54
+ }
55
+ .date-range-picker > * {
56
+ min-width: 0;
57
+ }
58
+ .date-range-picker .date-picker {
59
+ margin-inline: 0;
60
+ }
61
+ .date-range-picker .date-picker:first-child {
62
+ margin-inline-end: 0.5rem;
63
+ }
64
+ .date-range-picker .checkbox {
65
+ margin-inline-start: 0.25rem;
66
+ }
67
+
68
+ .error-message {
69
+ color: red;
70
+ font-size: 0.875rem;
71
+ margin-top: 0.5rem;
72
+ }
73
+ </style>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ icon?: string;
4
+ title: string;
5
+ description?: string;
6
+ size?: 'small' | 'medium' | 'large';
7
+ }
8
+
9
+ const props = withDefaults(defineProps<Props>(), {
10
+ size: 'medium',
11
+ });
12
+ </script>
13
+
14
+ <template>
15
+ <div class="empty-state" :class="size">
16
+ <orio-icon v-if="icon" :name="icon" class="empty-state-icon" />
17
+ <orio-view-text
18
+ type="title"
19
+ :size="size === 'large' ? 'medium' : 'small'"
20
+ class="empty-state-title"
21
+ >
22
+ {{ title }}
23
+ </orio-view-text>
24
+ <orio-view-text
25
+ v-if="description"
26
+ type="subtitle"
27
+ class="empty-state-description"
28
+ >
29
+ {{ description }}
30
+ </orio-view-text>
31
+ <slot />
32
+ </div>
33
+ </template>
34
+
35
+ <style scoped>
36
+ .empty-state {
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: center;
40
+ justify-content: center;
41
+ text-align: center;
42
+ gap: 0.5rem;
43
+ padding: 2rem;
44
+ color: var(--color-text-secondary);
45
+ }
46
+ .empty-state.small {
47
+ padding: 1rem;
48
+ gap: 0.25rem;
49
+ }
50
+ .empty-state.small .empty-state-icon {
51
+ font-size: 2rem;
52
+ }
53
+ .empty-state.medium {
54
+ padding: 2rem;
55
+ gap: 0.5rem;
56
+ }
57
+ .empty-state.medium .empty-state-icon {
58
+ font-size: 3rem;
59
+ }
60
+ .empty-state.large {
61
+ padding: 3rem;
62
+ gap: 1rem;
63
+ }
64
+ .empty-state.large .empty-state-icon {
65
+ font-size: 4rem;
66
+ }
67
+
68
+ .empty-state-icon {
69
+ color: var(--color-text-tertiary);
70
+ opacity: 0.5;
71
+ }
72
+
73
+ .empty-state-title {
74
+ color: var(--color-text-secondary);
75
+ }
76
+
77
+ .empty-state-description {
78
+ color: var(--color-text-tertiary);
79
+ max-width: 30ch;
80
+ }
81
+ </style>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { iconRegistry, type IconName } from '../utils/icon-registry'
4
+
5
+ export interface IconProps {
6
+ name: IconName | string
7
+ size?: string | number
8
+ color?: string
9
+ }
10
+
11
+ const props = withDefaults(defineProps<IconProps>(), {
12
+ size: '1em',
13
+ color: 'currentColor'
14
+ })
15
+
16
+ const iconSvg = computed(() => iconRegistry[props.name] || '')
17
+ const sizeValue = computed(() =>
18
+ typeof props.size === 'number' ? `${props.size}px` : props.size
19
+ )
20
+ </script>
21
+
22
+ <template>
23
+ <span
24
+ class="orio-icon"
25
+ :style="{
26
+ width: sizeValue,
27
+ height: sizeValue,
28
+ color: color,
29
+ display: 'inline-flex',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ flexShrink: 0
33
+ }"
34
+ v-html="iconSvg"
35
+ />
36
+ </template>
37
+
38
+ <style scoped>
39
+ .orio-icon{align-items:center;display:inline-flex;flex-shrink:0;justify-content:center}.orio-icon :deep(svg){fill:currentColor;height:100%;width:100%}
40
+ </style>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ defineEmits<{
3
+ (e: 'input', value: string): void;
4
+ }>();
5
+
6
+ const text = defineModel<string>({ default: '' });
7
+ </script>
8
+
9
+ <template>
10
+ <orio-control-element v-bind="$attrs">
11
+ <input
12
+ v-bind="$attrs"
13
+ v-model="text"
14
+ type="text"
15
+ class="text-input"
16
+ >
17
+ </orio-control-element>
18
+ </template>
19
+
20
+ <style scoped>
21
+ .text-input {
22
+ width: 100%;
23
+ padding: 0.5rem 0.75rem;
24
+ border: 1px solid var(--color-border);
25
+ border-radius: 4px;
26
+ font-size: 1rem;
27
+ color: var(--color-text);
28
+ background-color: var(--color-bg);
29
+ box-sizing: border-box;
30
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
31
+ }
32
+ .text-input::placeholder {
33
+ color: var(--color-muted);
34
+ }
35
+ .text-input:hover {
36
+ border-color: var(--color-accent);
37
+ }
38
+ .text-input:focus {
39
+ border-color: var(--color-accent);
40
+ box-shadow: 0 0 0 2px var(--color-accent-soft);
41
+ outline: none;
42
+ }
43
+ .text-input:disabled {
44
+ background-color: var(--color-surface);
45
+ color: var(--color-muted);
46
+ cursor: not-allowed;
47
+ }
48
+ </style>
@@ -0,0 +1,6 @@
1
+ <script setup lang="ts">
2
+ // Loading spinner using the bundled loading-loop icon
3
+ </script>
4
+ <template>
5
+ <orio-icon name="loading-loop" />
6
+ </template>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import { ref, nextTick, watch } from 'vue';
3
+ import { useWindowSize, useTimeoutFn } from '@vueuse/core';
4
+
5
+ export interface OriginRect {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+
12
+ const props = defineProps<{
13
+ origin: OriginRect | null;
14
+ }>();
15
+
16
+ const show = defineModel<Boolean>('show');
17
+
18
+ const wrapper = ref<HTMLDivElement | null>(null);
19
+
20
+ const { width: windowWidth, height: windowHeight } = useWindowSize();
21
+
22
+ function animateToCenter(el: HTMLDivElement) {
23
+ useTimeoutFn(() => {
24
+ requestAnimationFrame(() => {
25
+ el.style.transform = 'translate(0, 0) scale(1)';
26
+ el.style.opacity = '1';
27
+ });
28
+ }, 100);
29
+ }
30
+
31
+ async function animateOpen() {
32
+ await nextTick();
33
+
34
+ const el = wrapper.value;
35
+ if (!el) return;
36
+ if (!props.origin) {
37
+ animateToCenter(el);
38
+ return;
39
+ }
40
+
41
+ const { x, y, width, height } = props.origin;
42
+
43
+ el.style.transform = `
44
+ translate(${x - windowWidth.value / 2}px, ${y - windowHeight.value / 2}px)
45
+ scale(${width / el.offsetWidth}, ${height / el.offsetHeight})
46
+ `;
47
+ el.style.opacity = '0.5';
48
+
49
+ animateToCenter(el);
50
+ }
51
+
52
+ watch(show, (open) => {
53
+ if (open) animateOpen();
54
+ });
55
+ </script>
56
+
57
+ <template>
58
+ <transition name="overlay-fade">
59
+ <div v-if="show" class="overlay" @click.self="show = false">
60
+ <div ref="wrapper" class="modal">
61
+ <slot />
62
+ </div>
63
+ </div>
64
+ </transition>
65
+ </template>
66
+
67
+ <style scoped>
68
+ .overlay{align-items:center;backdrop-filter:blur(6px);background:rgba(0,0,0,.45);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .25s ease}.modal{background:#fff;border-radius:1rem;box-shadow:0 25px 60px rgba(0,0,0,.25);color:var(--color-muted);max-height:90vh;max-width:90vw;padding:24px;position:absolute;transform-origin:top left;transition:transform .35s cubic-bezier(.2,.8,.2,1),opacity .3s ease;width:380px}.modal,.overlay-fade-enter-from,.overlay-fade-leave-to{opacity:0}.overlay-fade-enter-active,.overlay-fade-leave-active{transition:opacity .25s ease}
69
+ </style>
@@ -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 scoped>
243
+ .popover {
244
+ border: 0;
245
+ background-color: transparent;
246
+ position: fixed;
247
+ z-index: 1;
248
+ }
249
+ </style>