@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,63 @@
1
+ <template>
2
+ <v-container class="pa-0">
3
+ <v-text-field
4
+ ref="input"
5
+ v-model="answer"
6
+ prepend-icon="edit"
7
+ :autofocus="autofocus"
8
+ row-height="24"
9
+ toggle-keys="[13,32]"
10
+ class="text-h5"
11
+ :rules="[isNumeric]"
12
+ @keyup.enter="submitAnswer(makeNumeric(answer))"
13
+ ></v-text-field>
14
+ </v-container>
15
+ </template>
16
+
17
+ <script lang="ts">
18
+ import { Answer } from '@vue-skuilder/common';
19
+ import UserInput from './BaseUserInput';
20
+ import { defineComponent } from 'vue';
21
+
22
+ type InputNumberRefs = {
23
+ input: HTMLInputElement;
24
+ };
25
+
26
+ type InputNumberInstance = ReturnType<typeof defineComponent> & {
27
+ $refs: InputNumberRefs;
28
+ };
29
+
30
+ export default defineComponent({
31
+ name: 'UserInputNumber',
32
+
33
+ ref: {} as InputNumberRefs,
34
+
35
+ extends: UserInput,
36
+
37
+ methods: {
38
+ mounted(this: InputNumberInstance) {
39
+ this.$refs.input.focus();
40
+ },
41
+
42
+ isNumeric(s: string): boolean {
43
+ return !isNaN(Number.parseFloat(s));
44
+ },
45
+
46
+ makeNumeric(num: Answer): number {
47
+ if (typeof num === 'string') {
48
+ return Number.parseFloat(num);
49
+ } else {
50
+ throw new Error('Expected a string, got ' + typeof num);
51
+ }
52
+ },
53
+ },
54
+ });
55
+ </script>
56
+
57
+ <style scoped>
58
+ .userInput {
59
+ border: none;
60
+ text-align: center;
61
+ border-bottom: 1px black;
62
+ }
63
+ </style>
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <span class="user-input-container">
3
+ <input
4
+ v-model="answer"
5
+ :autofocus="autofocus"
6
+ type="text"
7
+ class="user-input-string"
8
+ ref="input"
9
+ @keyup.enter="submitAnswer(answer)"
10
+ />
11
+ </span>
12
+ </template>
13
+
14
+ <script lang="ts">
15
+ // @compositionstart="onCompositionStart"
16
+ // @compositionend="onCompositionEnd"
17
+ import UserInput from './BaseUserInput';
18
+ import { defineComponent } from 'vue';
19
+
20
+ export default defineComponent({
21
+ name: 'UserInputString',
22
+ extends: UserInput,
23
+
24
+ props: {
25
+ icon: {
26
+ type: Boolean,
27
+ required: false,
28
+ },
29
+ },
30
+
31
+ computed: {
32
+ prependIcon(): string {
33
+ return this.icon ? 'edit' : '';
34
+ },
35
+ },
36
+
37
+ mounted() {
38
+ (this.$refs.input as HTMLInputElement)?.focus();
39
+ },
40
+
41
+ methods: {
42
+ // debugInput(e: Event) {
43
+ // const target = e.target as HTMLInputElement;
44
+ // console.log('Input event:', {
45
+ // value: target.value,
46
+ // currentAnswer: this.answer,
47
+ // target,
48
+ // });
49
+ // },
50
+ // debugKeyup(e: KeyboardEvent) {
51
+ // const target = e.target as HTMLInputElement;
52
+ // console.log('Keyup event:', {
53
+ // key: e.key,
54
+ // value: target.value,
55
+ // currentAnswer: this.answer,
56
+ // target,
57
+ // });
58
+ // },
59
+ },
60
+ });
61
+ </script>
62
+
63
+ <style scoped>
64
+ .user-input-container {
65
+ display: inline-block;
66
+ min-width: 6em;
67
+ vertical-align: baseline;
68
+ }
69
+
70
+ .user-input-string {
71
+ display: inline-block;
72
+ background: transparent;
73
+ border: none;
74
+ border-bottom: 1px solid currentColor;
75
+ color: currentColor;
76
+ font-family: inherit;
77
+ font-size: inherit;
78
+ line-height: inherit;
79
+ padding: 0;
80
+ margin: 0;
81
+ text-align: center;
82
+ width: 100%;
83
+ outline: none;
84
+ }
85
+
86
+ .user-input-string:focus {
87
+ border-bottom: 2px solid currentColor;
88
+ }
89
+ </style>
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <span v-if="radioType" class="text-h5 underline"
3
+ >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span
4
+ >
5
+ <user-input-string v-else id="input" :icon="false" type="text" :value="processedText" />
6
+ </template>
7
+
8
+ <script lang="ts">
9
+ import { defineComponent, ref, computed, onMounted } from 'vue';
10
+ import UserInputString from './UserInputString.vue';
11
+
12
+ export default defineComponent({
13
+ name: 'FillInInput',
14
+
15
+ components: {
16
+ UserInputString,
17
+ },
18
+
19
+ props: {
20
+ text: {
21
+ type: String,
22
+ required: true,
23
+ },
24
+ },
25
+
26
+ setup(props) {
27
+ // State
28
+ const inputType = ref<'text' | 'radio'>('text');
29
+ const processedText = ref('');
30
+
31
+ // Computed
32
+ const radioType = computed(() => {
33
+ return props.text.split('||').length > 1;
34
+ });
35
+
36
+ // Process text on mount
37
+ onMounted(() => {
38
+ console.log(`fillinCreated w/ text: ${props.text}`);
39
+
40
+ // Remove mustache syntax
41
+ processedText.value = props.text.substring(2, props.text.length - 2);
42
+
43
+ console.log(`fillin text trimmed to: ${processedText.value}`);
44
+
45
+ const split = processedText.value.split('||');
46
+ if (split.length > 1) {
47
+ inputType.value = 'radio';
48
+ }
49
+ });
50
+
51
+ return {
52
+ inputType,
53
+ radioType,
54
+ processedText,
55
+ };
56
+ },
57
+ });
58
+ </script>
59
+
60
+ <style scoped>
61
+ #input {
62
+ display: inline-block;
63
+ min-width: 4em;
64
+ text-align: center;
65
+ }
66
+
67
+ .underline {
68
+ text-decoration: underline;
69
+ text-decoration-style: solid 14px;
70
+ }
71
+ </style>
@@ -0,0 +1,180 @@
1
+ // src/base-course/CompositionViewable.ts
2
+
3
+ import moment from 'moment';
4
+ import { computed, ComputedRef, ref, Ref } from 'vue';
5
+ import { CardRecord, QuestionRecord } from '@vue-skuilder/db';
6
+ import { HotKey } from '../utils/SkldrMouseTrap';
7
+ import { Question } from './Displayable';
8
+ import { ViewData, Answer } from '@vue-skuilder/common';
9
+
10
+ // Core interfaces to ensure type safety
11
+ export interface ViewableUtils {
12
+ startTime: Ref<moment.Moment>;
13
+ hotKeys: Ref<HotKey[]>;
14
+ timeSpent: ComputedRef<number>;
15
+ logger: ViewableLogger;
16
+ getURL: (item: string, dataShapeIndex?: number) => string;
17
+ emitResponse: (record: CardRecord) => void;
18
+ }
19
+
20
+ export interface ViewableLogger {
21
+ log: (message?: unknown, ...params: unknown[]) => void;
22
+ error: (message?: unknown, ...params: unknown[]) => void;
23
+ warn: (message?: unknown, ...params: unknown[]) => void;
24
+ }
25
+
26
+ export interface QuestionViewUtils<Q extends Question> {
27
+ priorSessionViews: Ref<number>;
28
+ priorAttempts: Ref<number>;
29
+ priorAnswers: Ref<[Answer, string][]>;
30
+ maxAttemptsPerView: Ref<number>;
31
+ maxSessionViews: Ref<number>;
32
+ question: Ref<Q | undefined>;
33
+ submitAnswer: (answer: Answer, submittingClass?: string) => QuestionRecord;
34
+ }
35
+
36
+ // Base composable for viewable functionality
37
+ export function useViewable(
38
+ props: { data: ViewData[] },
39
+ emit: (event: string, ...args: unknown[]) => void,
40
+ componentName: string
41
+ ): ViewableUtils {
42
+ const startTime = ref(moment.utc());
43
+ const hotKeys = ref<HotKey[]>([]);
44
+
45
+ const logger: ViewableLogger = {
46
+ log: (message?: unknown, ...params: unknown[]) =>
47
+ console.log(`[${componentName}]: `, message, ...params),
48
+ error: (message?: unknown, ...params: unknown[]) =>
49
+ console.error(`[${componentName}]: `, message, ...params),
50
+ warn: (message?: unknown, ...params: unknown[]) =>
51
+ console.warn(`[${componentName}]: `, message, ...params),
52
+ };
53
+
54
+ const timeSpent = computed(() => Math.abs(moment.utc().diff(startTime.value, 'milliseconds')));
55
+
56
+ const getURL = (item: string, dataShapeIndex = 0): string => {
57
+ try {
58
+ if (props.data[dataShapeIndex]?.[item]) {
59
+ return URL.createObjectURL(props.data[dataShapeIndex][item] as Blob);
60
+ }
61
+ } catch (error) {
62
+ logger.error('Error creating URL for item:', item, error);
63
+ }
64
+ return '';
65
+ };
66
+
67
+ const emitResponse = (record: CardRecord) => {
68
+ emit('emitResponse', record);
69
+ };
70
+
71
+ return {
72
+ startTime,
73
+ hotKeys,
74
+ timeSpent,
75
+ logger,
76
+ getURL,
77
+ emitResponse,
78
+ };
79
+ }
80
+
81
+ // Question view composable
82
+ export function useQuestionView<Q extends Question>(
83
+ viewableUtils: ViewableUtils
84
+ // modifyDifficulty?: number
85
+ ): QuestionViewUtils<Q> {
86
+ const priorSessionViews = ref(0);
87
+ const priorAttempts = ref(0);
88
+ const priorAnswers = ref<[Answer, string][]>([]);
89
+ const maxAttemptsPerView = ref(3);
90
+ const maxSessionViews = ref(1);
91
+ const question = ref<Q>();
92
+
93
+ const submitAnswer = (answer: Answer, submittingClass?: string): QuestionRecord => {
94
+ viewableUtils.logger.log('submitAnswer called...');
95
+
96
+ if (!question.value) {
97
+ throw new Error('Question not initialized');
98
+ }
99
+
100
+ priorAnswers.value.push([answer, submittingClass ?? '']);
101
+
102
+ const evaluation = question.value.evaluate(answer, viewableUtils.timeSpent.value);
103
+
104
+ viewableUtils.logger.log(`evaluation of answer ${answer}:`, evaluation);
105
+
106
+ const record: QuestionRecord = {
107
+ ...evaluation,
108
+ priorAttemps: priorAttempts.value,
109
+ courseID: '',
110
+ cardID: '',
111
+ timeSpent: viewableUtils.timeSpent.value,
112
+ timeStamp: viewableUtils.startTime.value,
113
+ userAnswer: answer,
114
+ };
115
+
116
+ if (!evaluation.isCorrect) {
117
+ priorAttempts.value++;
118
+ }
119
+
120
+ viewableUtils.emitResponse(record);
121
+ return record;
122
+ };
123
+
124
+ return {
125
+ priorSessionViews,
126
+ priorAttempts,
127
+ priorAnswers,
128
+ maxAttemptsPerView,
129
+ maxSessionViews,
130
+ question,
131
+ submitAnswer,
132
+ };
133
+ }
134
+
135
+ // Helper functions
136
+ export function isQuestionView(component: any): component is QuestionViewUtils<Question> {
137
+ return 'submitAnswer' in component && 'priorAttempts' in component;
138
+ }
139
+
140
+ // Example usage in a child component:
141
+ /*
142
+ import { defineComponent, PropType } from 'vue';
143
+ import { useViewable, useQuestionView } from './CompositionViewable';
144
+ import { MyCustomQuestion } from './types';
145
+
146
+ export default defineComponent({
147
+ name: 'MyCustomQuestionView',
148
+ props: {
149
+ data: {
150
+ type: Array as PropType<ViewData[]>,
151
+ required: true,
152
+ },
153
+ modifyDifficulty: Number,
154
+ },
155
+ setup(props, { emit }) {
156
+ const viewableUtils = useViewable(props, emit, 'MyCustomQuestionView');
157
+ const questionUtils = useQuestionView<MyCustomQuestion>(viewableUtils, props.modifyDifficulty);
158
+
159
+ // Initialize question
160
+ questionUtils.question.value = new MyCustomQuestion(props.data);
161
+
162
+ // Add custom logic here
163
+ const customMethod = () => {
164
+ viewableUtils.logger.log('Custom method called');
165
+ // ...
166
+ };
167
+
168
+ return {
169
+ ...viewableUtils,
170
+ ...questionUtils,
171
+ customMethod,
172
+ };
173
+ },
174
+ template: `
175
+ <div data-viewable="MyCustomQuestionView">
176
+ <!-- Your template here -->
177
+ </div>
178
+ `
179
+ });
180
+ */
@@ -0,0 +1,133 @@
1
+ import { type DefineComponent, defineComponent } from 'vue';
2
+ import { FieldType, DataShape, ViewData, Answer, Evaluation } from '@vue-skuilder/common';
3
+
4
+ // [ ] #vue3 - post migration, specify this more precisely (no longer a hodge-podge)
5
+ export type ViewComponent =
6
+ | DefineComponent<unknown, unknown, unknown>
7
+ | ReturnType<typeof defineComponent>;
8
+
9
+ export function isDefineComponent(
10
+ v: ViewComponent
11
+ ): v is DefineComponent<unknown, unknown, unknown> {
12
+ return (v as DefineComponent<unknown, unknown, unknown>).__isFragment !== undefined;
13
+ }
14
+
15
+ // export function isComponentOptions(v: ViewComponent): v is ComponentOptions<any> {
16
+ // return (v as ComponentOptions<Vue>).name !== undefined;
17
+ // }
18
+
19
+ // tslint:disable-next-line:max-classes-per-file
20
+ export abstract class Displayable {
21
+ public static dataShapes: DataShape[];
22
+
23
+ public static views: Array<ViewComponent>;
24
+ public static seedData?: Array<any>;
25
+ /**
26
+ * True if this displayable content type is meant to have
27
+ * user-submitted questions. False if supplied seedData array
28
+ * is comprehensive for the content type. EG, a SingleDigitAddition
29
+ * type may comprehensively supply 0+0,0+1,...,9+9 as its seed
30
+ * data, and not want any user input.
31
+ */
32
+ public static acceptsUserData: boolean = true;
33
+
34
+ /**
35
+ *
36
+ */
37
+ constructor(viewData: ViewData[]) {
38
+ if (viewData.length === 0) {
39
+ throw new Error(`
40
+ Displayable Constructor was called with no view Data.
41
+ `);
42
+ }
43
+ validateData(this.dataShapes(), viewData);
44
+ }
45
+
46
+ public abstract dataShapes(): DataShape[];
47
+ public abstract views(): Array<ViewComponent>;
48
+ }
49
+
50
+ function validateData(shape: DataShape[], data: ViewData[]) {
51
+ for (let i = 0; i < shape.length; i++) {
52
+ shape[i].fields.forEach((field, j) => {
53
+ console.log(`[Displayable] shape[${i}].field[${j}]:\n ${JSON.stringify(field)}`);
54
+ if (data[i][field.name] === undefined && field.type !== FieldType.MEDIA_UPLOADS) {
55
+ // throw new Error(`field validation failed:\n\t${field.name}, (${field.type})`);
56
+ console.warn(`[Displayable] missing data`);
57
+ }
58
+ });
59
+ }
60
+ }
61
+
62
+ // tslint:disable-next-line:max-classes-per-file
63
+ export abstract class Question extends Displayable {
64
+ /**
65
+ * returns a yes/no evaluation of a user's answer. Informs the SRS
66
+ * algorithm's decision to expand or reset a card's spacing
67
+ * @param answer
68
+ */
69
+ protected abstract isCorrect(answer: Answer): boolean;
70
+ /**
71
+ * returns a number from [0,1] representing the user's performance on the question,
72
+ * which informs elo adjustments and SRS multipliers
73
+ *
74
+ * @param answer the user's answer
75
+ * @param timeSpent the time the user spent on the card in milliseconds
76
+ * @returns a rating of the user's displayed skill, from 0-1
77
+ */
78
+ protected displayedSkill(answer: Answer, timeSpent: number): number {
79
+ console.warn(`Question is running the reference implementation of displayedSkill.
80
+ Consider overriding!`);
81
+ // experts should answer this question in <= 5 secnods (5000 ms)
82
+ const expertSpeed = 5000;
83
+ const userSpeed = Math.min(timeSpent, 10 * expertSpeed);
84
+
85
+ // if userResponse is > 10 x expertSpeed, discount as probably afk / distracted ?
86
+
87
+ const speedPenalty = userSpeed / expertSpeed;
88
+ const speedPenaltyMultiplier = userSpeed > expertSpeed ? Math.pow(0.8, speedPenalty) : 1;
89
+
90
+ let ret = this.isCorrect(answer) ? 1 : 0;
91
+
92
+ ret = ret * speedPenaltyMultiplier;
93
+
94
+ return Math.min(ret, 1);
95
+ }
96
+
97
+ /**
98
+ *
99
+ * @param answer the student's answer
100
+ * @param timeSpent the amount of time spent in ms
101
+ * @returns
102
+ */
103
+ public evaluate(answer: Answer, timeSpent: number): Evaluation {
104
+ return {
105
+ isCorrect: this.isCorrect(answer),
106
+ performance: this.displayedSkill(answer, timeSpent),
107
+ };
108
+ }
109
+
110
+ /*
111
+ TODO:
112
+
113
+ This class and its interface are critical in this app - it defines the app's
114
+ methodology, or its architecture of methodologies, for making inferences based on
115
+ student performance.
116
+
117
+ Some future directions:
118
+
119
+ displayedSkill() should receive contextual data as well as the direct report of
120
+ a user's interaction with this card. Context includes, eg, the status of the current
121
+ study session (is the user on tilt? have there been leading Qs? have there been distractors?),
122
+ the status of the user's interaction with the card (new? mature? history of resets?)
123
+
124
+ displayedSkill() should reach out to a course (or just the card, or card-type?) in order to receive
125
+ custom skill-dimension evaluators. EG: a card in the ear training course tagged 'articulation'
126
+ could trigger a fetch of a particular evaluator fcn for articulation. In general, we
127
+ want high-dimension evaluations.
128
+
129
+ answers should be processed for the likelihood of non-substantive-incorrectness. eg,
130
+ 7 * 4 = 8 is much more likely to be a typo than a substantive error when compared
131
+ with 7 * 4 = 21
132
+ */
133
+ }
@@ -0,0 +1,2 @@
1
+ export * from './CompositionViewable';
2
+ export * from './Displayable';
package/src/index.ts ADDED
@@ -0,0 +1,79 @@
1
+ // // Import and re-export components
2
+ // import './styles/index.scss';
3
+
4
+ export { default as HeatMap } from './components/HeatMap.vue';
5
+ export type { DayData, Color, ActivityRecord } from './components/HeatMap.types';
6
+
7
+ // global keybinding controller
8
+ export { default as SkMouseTrap } from './components/SkMouseTrap.vue';
9
+ export { default as SkMouseTrapToolTip } from './components/SkMouseTrapToolTip.vue';
10
+ export * from './components/SkMouseTrap.types';
11
+ export * from './components/SkMouseTrapToolTip.types';
12
+ export { SkldrMouseTrap } from './utils/SkldrMouseTrap';
13
+ export type { HotKey, HotKeyMetaData } from './utils/SkldrMouseTrap';
14
+
15
+ // snackbar service - user notifications
16
+ export { default as SnackbarService } from './components/SnackbarService.vue';
17
+ export { alertUser, type SnackbarOptions } from './components/SnackbarService';
18
+
19
+ export { default as PaginatingToolbar } from './components/PaginatingToolbar.vue';
20
+ export * from './components/PaginatingToolbar.types';
21
+
22
+ // Composables
23
+ export * from './composables/CompositionViewable';
24
+ export * from './composables/Displayable';
25
+
26
+ /*
27
+ Study Session Components
28
+ */
29
+
30
+ export { default as StudySession } from './components/StudySession.vue';
31
+ export { default as StudySessionTimer } from './components/StudySessionTimer.vue';
32
+ export type { StudySessionConfig } from './components/StudySession.types';
33
+
34
+ /*
35
+ studentInputs
36
+
37
+ These components
38
+ - embed into cards
39
+ - accept inputs (answers) from students
40
+ - emit / pass answers to other components in the system
41
+ */
42
+ export * from './components/studentInputs/BaseUserInput';
43
+ export { default as RadioMultipleChoice } from './components/studentInputs/RadioMultipleChoice.vue';
44
+ export { default as MultipleChoiceOption } from './components/studentInputs/MultipleChoiceOption.vue';
45
+ export { default as TFSelect } from './components/studentInputs/TrueFalse.vue';
46
+ export { default as UserInputNumber } from './components/studentInputs/UserInputNumber.vue';
47
+ export { default as UserInputString } from './components/studentInputs/UserInputString.vue';
48
+ export { default as FillInInput } from './components/studentInputs/fillInInput.vue';
49
+
50
+ /*
51
+ cardRendering
52
+
53
+ These components are used to render course content
54
+ */
55
+ export { default as CardViewer } from './components/cardRendering/CardViewer.vue';
56
+ export { default as CardLoader } from './components/cardRendering/CardLoader.vue';
57
+
58
+ export * from './components/cardRendering/MarkdownRendererHelpers';
59
+ export { default as AudioAutoPlayer } from './components/cardRendering/AudioAutoPlayer.vue';
60
+ export { default as CodeBlockRenderer } from './components/cardRendering/CodeBlockRenderer.vue';
61
+ export { default as MdTokenRenderer } from './components/cardRendering/MdTokenRenderer.vue';
62
+ export { default as MarkdownRenderer } from './components/cardRendering/MarkdownRenderer.vue';
63
+
64
+ /*
65
+ Authentication Components
66
+ */
67
+ export * from './components/auth';
68
+
69
+ /*
70
+ stores
71
+ */
72
+ export * from './stores/useCardPreviewModeStore';
73
+ export * from './stores/useAuthStore';
74
+ export * from './stores/useConfigStore';
75
+
76
+ /*
77
+ plugins
78
+ */
79
+ export { piniaPlugin } from './plugins/pinia';
@@ -0,0 +1,24 @@
1
+ // common-ui/src/plugins/pinia.ts
2
+ import { Plugin } from 'vue';
3
+ import { Pinia } from 'pinia';
4
+
5
+ // This will hold the Pinia instance provided by the main app
6
+ let _pinia: Pinia | null = null;
7
+
8
+ export const setPinia = (pinia: Pinia) => {
9
+ _pinia = pinia;
10
+ };
11
+
12
+ export const getPinia = (): Pinia | null => {
13
+ return _pinia;
14
+ };
15
+
16
+ // Create a plugin that the main app can use
17
+ export const piniaPlugin: Plugin = {
18
+ install(app, options) {
19
+ const pinia = options?.pinia;
20
+ if (pinia) {
21
+ setPinia(pinia);
22
+ }
23
+ },
24
+ };