@vue-skuilder/common-ui 0.1.1

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 (68) hide show
  1. package/dist/assets/index.css +10 -0
  2. package/dist/common-ui.es.js +16404 -0
  3. package/dist/common-ui.es.js.map +1 -0
  4. package/dist/common-ui.umd.js +9 -0
  5. package/dist/common-ui.umd.js.map +1 -0
  6. package/dist/components/HeatMap.types.d.ts +13 -0
  7. package/dist/components/PaginatingToolbar.types.d.ts +40 -0
  8. package/dist/components/SkMouseTrap.types.d.ts +3 -0
  9. package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
  10. package/dist/components/SnackbarService.d.ts +11 -0
  11. package/dist/components/StudySession.types.d.ts +6 -0
  12. package/dist/components/auth/index.d.ts +4 -0
  13. package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
  14. package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
  15. package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
  16. package/dist/composables/CompositionViewable.d.ts +33 -0
  17. package/dist/composables/Displayable.d.ts +47 -0
  18. package/dist/composables/index.d.ts +2 -0
  19. package/dist/index.d.ts +36 -0
  20. package/dist/plugins/pinia.d.ts +5 -0
  21. package/dist/stores/useAuthStore.d.ts +225 -0
  22. package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
  23. package/dist/stores/useConfigStore.d.ts +11 -0
  24. package/dist/utils/SkldrMouseTrap.d.ts +32 -0
  25. package/package.json +67 -0
  26. package/src/components/HeatMap.types.ts +15 -0
  27. package/src/components/HeatMap.vue +354 -0
  28. package/src/components/PaginatingToolbar.types.ts +48 -0
  29. package/src/components/PaginatingToolbar.vue +75 -0
  30. package/src/components/SkMouseTrap.types.ts +3 -0
  31. package/src/components/SkMouseTrap.vue +70 -0
  32. package/src/components/SkMouseTrapToolTip.types.ts +41 -0
  33. package/src/components/SkMouseTrapToolTip.vue +316 -0
  34. package/src/components/SnackbarService.ts +39 -0
  35. package/src/components/SnackbarService.vue +71 -0
  36. package/src/components/StudySession.types.ts +6 -0
  37. package/src/components/StudySession.vue +670 -0
  38. package/src/components/StudySessionTimer.vue +121 -0
  39. package/src/components/auth/UserChip.vue +106 -0
  40. package/src/components/auth/UserLogin.vue +141 -0
  41. package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
  42. package/src/components/auth/UserRegistration.vue +181 -0
  43. package/src/components/auth/index.ts +4 -0
  44. package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
  45. package/src/components/cardRendering/CardLoader.vue +123 -0
  46. package/src/components/cardRendering/CardViewer.vue +101 -0
  47. package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
  48. package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
  49. package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
  50. package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
  51. package/src/components/studentInputs/BaseUserInput.ts +71 -0
  52. package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
  53. package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
  54. package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
  55. package/src/components/studentInputs/TrueFalse.vue +27 -0
  56. package/src/components/studentInputs/UserInputNumber.vue +63 -0
  57. package/src/components/studentInputs/UserInputString.vue +89 -0
  58. package/src/components/studentInputs/fillInInput.vue +71 -0
  59. package/src/composables/CompositionViewable.ts +180 -0
  60. package/src/composables/Displayable.ts +133 -0
  61. package/src/composables/index.ts +2 -0
  62. package/src/index.ts +79 -0
  63. package/src/plugins/pinia.ts +24 -0
  64. package/src/stores/useAuthStore.ts +92 -0
  65. package/src/stores/useCardPreviewModeStore.ts +32 -0
  66. package/src/stores/useConfigStore.ts +60 -0
  67. package/src/utils/SkldrMouseTrap.ts +141 -0
  68. package/src/vue-shims.d.ts +5 -0
@@ -0,0 +1,316 @@
1
+ <template>
2
+ <div
3
+ class="sk-mousetrap-tooltip-wrapper"
4
+ ref="wrapperElement"
5
+ :class="[
6
+ isControlKeyPressed && !disabled && highlightEffect !== 'none' ? `sk-mousetrap-highlight-${highlightEffect}` : '',
7
+ ]"
8
+ >
9
+ <slot></slot>
10
+ <transition name="fade">
11
+ <div
12
+ v-if="showTooltip && isControlKeyPressed && !disabled"
13
+ class="sk-mousetrap-tooltip"
14
+ :class="{
15
+ 'sk-mt-tooltip-top': position === 'top',
16
+ 'sk-mt-tooltip-bottom': position === 'bottom',
17
+ 'sk-mt-tooltip-left': position === 'left',
18
+ 'sk-mt-tooltip-right': position === 'right',
19
+ }"
20
+ >
21
+ {{ formattedHotkey }}
22
+ </div>
23
+ </transition>
24
+ </div>
25
+ </template>
26
+
27
+ <script lang="ts">
28
+ import { defineComponent, PropType, ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
29
+ import { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';
30
+
31
+ export default defineComponent({
32
+ name: 'SkMouseTrapToolTip',
33
+
34
+ props: {
35
+ hotkey: {
36
+ type: [String, Array] as PropType<string | string[]>,
37
+ required: true,
38
+ },
39
+ command: {
40
+ type: String,
41
+ required: true,
42
+ },
43
+ disabled: {
44
+ type: Boolean,
45
+ default: false,
46
+ },
47
+ position: {
48
+ type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
49
+ default: 'top',
50
+ },
51
+ showTooltip: {
52
+ type: Boolean,
53
+ default: true,
54
+ },
55
+ highlightEffect: {
56
+ type: String as PropType<'glow' | 'scale' | 'border' | 'none'>,
57
+ default: 'glow',
58
+ },
59
+ },
60
+
61
+ emits: ['hotkey-triggered'],
62
+
63
+ setup(props, { emit }) {
64
+ const wrapperElement = ref<HTMLElement | null>(null);
65
+ const isControlKeyPressed = ref(false);
66
+ const hotkeyId = ref(`hotkey-${Math.random().toString(36).substring(2, 15)}`);
67
+
68
+ // Format hotkey for display
69
+ const formattedHotkey = computed(() => {
70
+ const hotkey = Array.isArray(props.hotkey) ? props.hotkey[0] : props.hotkey;
71
+ // Check if this is a sequence (has spaces) or a combination (has +)
72
+ if (hotkey.includes(' ')) {
73
+ // For sequences like "g h", display as "g, h"
74
+ return hotkey
75
+ .toLowerCase()
76
+ .split(' ')
77
+ .map((part) => part.charAt(0) + part.slice(1))
78
+ .join(', ');
79
+ } else {
80
+ // For combinations like "ctrl+s", display as "Ctrl + S"
81
+ return hotkey
82
+ .toLowerCase()
83
+ .split('+')
84
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
85
+ .join(' + ');
86
+ }
87
+ });
88
+
89
+ // Apply highlight effect to the actual button/control when Ctrl is pressed
90
+ watch(
91
+ () => isControlKeyPressed.value,
92
+ (pressed) => {
93
+ if (!wrapperElement.value || props.disabled) return;
94
+
95
+ const clickableElement = wrapperElement.value.querySelector(
96
+ 'button, a, input[type="button"], [role="button"]'
97
+ ) as HTMLElement;
98
+
99
+ if (clickableElement) {
100
+ clickableElement.style.transition = 'all 250ms ease';
101
+
102
+ if (pressed && props.highlightEffect !== 'none') {
103
+ // Add slight brightness increase to the inner element
104
+ clickableElement.style.filter = 'brightness(1.1)';
105
+ } else {
106
+ clickableElement.style.filter = '';
107
+ }
108
+ }
109
+ }
110
+ );
111
+
112
+ // Handle Ctrl key detection
113
+ const handleKeyDown = (e: KeyboardEvent) => {
114
+ if (e.key === 'Control') {
115
+ isControlKeyPressed.value = true;
116
+ }
117
+ };
118
+
119
+ const handleKeyUp = (e: KeyboardEvent) => {
120
+ if (e.key === 'Control') {
121
+ isControlKeyPressed.value = false;
122
+ }
123
+ };
124
+
125
+ // Handle clicking the element when hotkey is pressed
126
+ const handleHotkeyPress = () => {
127
+ if (props.disabled || !wrapperElement.value) return;
128
+
129
+ // Try finding a clickable element within our wrapper
130
+ let clickableElement = wrapperElement.value.querySelector(
131
+ 'button, a, input[type="button"], [role="button"]'
132
+ ) as HTMLElement;
133
+
134
+ // If no standard clickable element found, try to find navigation or list items
135
+ if (!clickableElement) {
136
+ clickableElement = wrapperElement.value.querySelector(
137
+ 'v-list-item, .v-list-item, router-link, .router-link, a, [to]'
138
+ ) as HTMLElement;
139
+ }
140
+
141
+ // If still no element found, try the wrapper itself - it might be clickable
142
+ if (
143
+ !clickableElement &&
144
+ (wrapperElement.value.hasAttribute('to') ||
145
+ wrapperElement.value.tagName === 'A' ||
146
+ wrapperElement.value.classList.contains('v-list-item'))
147
+ ) {
148
+ clickableElement = wrapperElement.value;
149
+ }
150
+
151
+ // Get closest parent list item or router link if we found a title/content element
152
+ if (!clickableElement) {
153
+ const closestClickableParent = wrapperElement.value.closest('v-list-item, .v-list-item, a, [to]');
154
+ if (closestClickableParent) {
155
+ clickableElement = closestClickableParent as HTMLElement;
156
+ }
157
+ }
158
+
159
+ if (clickableElement) {
160
+ if (clickableElement.hasAttribute('to')) {
161
+ // Handle router-link style navigation
162
+ const routePath = clickableElement.getAttribute('to');
163
+ if (routePath && window.location.pathname !== routePath) {
164
+ // Use parent router if available (Vue component)
165
+ const router = (window as any).$nuxt?.$router || (window as any).$router;
166
+ if (router && typeof router.push === 'function') {
167
+ router.push(routePath);
168
+ } else {
169
+ // Fallback to regular navigation
170
+ window.location.pathname = routePath;
171
+ }
172
+ }
173
+ emit('hotkey-triggered', props.hotkey);
174
+ } else {
175
+ // Regular click for standard elements
176
+ clickableElement.click();
177
+ emit('hotkey-triggered', props.hotkey);
178
+ }
179
+ } else {
180
+ // If no clickable element found, emit the event for parent handling
181
+ console.log('No clickable element found for hotkey', props.hotkey);
182
+ emit('hotkey-triggered', props.hotkey);
183
+ }
184
+ };
185
+
186
+ // Register/unregister the hotkey binding
187
+ const registerHotkey = () => {
188
+ if (!props.disabled) {
189
+ SkldrMouseTrap.addBinding({
190
+ hotkey: props.hotkey,
191
+ command: props.command,
192
+ callback: handleHotkeyPress,
193
+ });
194
+ }
195
+ };
196
+
197
+ const unregisterHotkey = () => {
198
+ if (!props.disabled) {
199
+ SkldrMouseTrap.removeBinding(props.hotkey);
200
+ }
201
+ };
202
+
203
+ // Watch for changes to the disabled prop
204
+ watch(
205
+ () => props.disabled,
206
+ (newValue) => {
207
+ if (newValue) {
208
+ unregisterHotkey();
209
+ } else {
210
+ registerHotkey();
211
+ }
212
+ }
213
+ );
214
+
215
+ onMounted(() => {
216
+ // Register global keyboard listeners for the Ctrl key
217
+ document.addEventListener('keydown', handleKeyDown);
218
+ document.addEventListener('keyup', handleKeyUp);
219
+
220
+ // Register the hotkey
221
+ registerHotkey();
222
+ });
223
+
224
+ onBeforeUnmount(() => {
225
+ // Clean up event listeners
226
+ document.removeEventListener('keydown', handleKeyDown);
227
+ document.removeEventListener('keyup', handleKeyUp);
228
+
229
+ // Unregister the hotkey
230
+ unregisterHotkey();
231
+ });
232
+
233
+ return {
234
+ wrapperElement,
235
+ isControlKeyPressed,
236
+ formattedHotkey,
237
+ };
238
+ },
239
+ });
240
+ </script>
241
+
242
+ <style scoped>
243
+ .sk-mousetrap-tooltip-wrapper {
244
+ display: inline-block;
245
+ position: relative;
246
+ }
247
+
248
+ .sk-mousetrap-tooltip {
249
+ position: absolute;
250
+ background-color: rgba(0, 0, 0, 0.8);
251
+ color: white;
252
+ padding: 2px 6px;
253
+ border-radius: 4px;
254
+ font-size: 12px;
255
+ white-space: nowrap;
256
+ pointer-events: none;
257
+ z-index: 9999;
258
+ }
259
+
260
+ .sk-mt-tooltip-top {
261
+ bottom: 100%;
262
+ margin-bottom: 5px;
263
+ left: 50%;
264
+ transform: translateX(-50%);
265
+ }
266
+
267
+ .sk-mt-tooltip-bottom {
268
+ top: 100%;
269
+ margin-top: 5px;
270
+ left: 50%;
271
+ transform: translateX(-50%);
272
+ }
273
+
274
+ .sk-mt-tooltip-left {
275
+ right: 100%;
276
+ margin-right: 5px;
277
+ top: 50%;
278
+ transform: translateY(-50%);
279
+ }
280
+
281
+ .sk-mt-tooltip-right {
282
+ left: 100%;
283
+ margin-left: 5px;
284
+ top: 50%;
285
+ transform: translateY(-50%);
286
+ }
287
+
288
+ /* Highlight effects when Ctrl is pressed */
289
+ .sk-mousetrap-highlight-glow {
290
+ box-shadow: 0 0 8px 2px rgba(25, 118, 210, 0.6);
291
+ transition: box-shadow 250ms ease;
292
+ }
293
+
294
+ .sk-mousetrap-highlight-scale {
295
+ transform: scale(1.03);
296
+ transition: transform 250ms ease;
297
+ }
298
+
299
+ .sk-mousetrap-highlight-border {
300
+ outline: 2px solid rgba(25, 118, 210, 0.8);
301
+ outline-offset: 2px;
302
+ border-radius: 4px;
303
+ transition: outline 250ms ease, outline-offset 250ms ease;
304
+ }
305
+
306
+ /* Fade transition */
307
+ .fade-enter-active,
308
+ .fade-leave-active {
309
+ transition: opacity 250ms ease;
310
+ }
311
+
312
+ .fade-enter-from,
313
+ .fade-leave-to {
314
+ opacity: 0;
315
+ }
316
+ </style>
@@ -0,0 +1,39 @@
1
+ // common-ui/src/components/SnackbarService.ts
2
+ import { Status } from '@vue-skuilder/common';
3
+
4
+ export interface SnackbarOptions {
5
+ text: string;
6
+ status: Status;
7
+ timeout?: number;
8
+ }
9
+
10
+ // Module for managing the snackbar service
11
+ const SnackbarServiceModule = (() => {
12
+ // Private variable to hold the instance
13
+ let _instance: { addSnack: (snack: SnackbarOptions) => void } | null = null;
14
+
15
+ return {
16
+ // Register the instance
17
+ setInstance(instance: { addSnack: (snack: SnackbarOptions) => void }): void {
18
+ _instance = instance;
19
+ },
20
+
21
+ // Get the current instance
22
+ getInstance(): { addSnack: (snack: SnackbarOptions) => void } | null {
23
+ return _instance;
24
+ },
25
+
26
+ // Alert user function
27
+ alertUser(msg: SnackbarOptions): void {
28
+ // Try getting the instance
29
+ const snackBarService = _instance;
30
+ if (snackBarService) {
31
+ snackBarService.addSnack(msg);
32
+ return;
33
+ }
34
+ console.error('SnackbarService not found');
35
+ },
36
+ };
37
+ })();
38
+
39
+ export const { setInstance, getInstance, alertUser } = SnackbarServiceModule;
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div>
3
+ <v-snackbar
4
+ v-for="snack in snacks"
5
+ :key="snacks.indexOf(snack)"
6
+ v-model="show[snacks.indexOf(snack)]"
7
+ :timeout="snack.timeout"
8
+ location="bottom right"
9
+ :color="getColor(snack)"
10
+ >
11
+ <div class="d-flex align-center justify-space-between w-100">
12
+ <span>{{ snack.text }}</span>
13
+ <v-btn icon variant="text" @click="close()">
14
+ <v-icon>mdi-close</v-icon>
15
+ </v-btn>
16
+ </div>
17
+ </v-snackbar>
18
+ </div>
19
+ </template>
20
+
21
+ <script lang="ts">
22
+ import { defineComponent } from 'vue';
23
+ import { Status } from '@vue-skuilder/common';
24
+ import { SnackbarOptions, setInstance } from './SnackbarService';
25
+
26
+ const SnackbarService = defineComponent({
27
+ name: 'SnackbarService',
28
+
29
+ data() {
30
+ return {
31
+ /**
32
+ * A history of snacks served in this session.
33
+ *
34
+ * Possible future work: write these to localstorage/pouchdb
35
+ * for persistance
36
+ */
37
+ snacks: [] as SnackbarOptions[],
38
+ show: [] as boolean[],
39
+ };
40
+ },
41
+ mounted() {
42
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
43
+ setInstance(this);
44
+ },
45
+
46
+ methods: {
47
+ addSnack(snack: SnackbarOptions): void {
48
+ this.snacks.push(snack);
49
+ this.show.push(true);
50
+ },
51
+
52
+ close(): void {
53
+ this.show.pop();
54
+ this.show.push(false);
55
+ },
56
+
57
+ getColor(snack: SnackbarOptions): string | undefined {
58
+ if (snack.status === Status.ok) {
59
+ return 'success';
60
+ } else if (snack.status === Status.error) {
61
+ return 'error';
62
+ } else if (snack.status === Status.warning) {
63
+ return 'yellow';
64
+ }
65
+ return undefined;
66
+ },
67
+ },
68
+ });
69
+
70
+ export default SnackbarService;
71
+ </script>
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Misc. config for a study StudySessionConfig
3
+ */
4
+ export type StudySessionConfig = {
5
+ likesConfetti: boolean;
6
+ };