@windward/core 0.27.0 → 0.29.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.
@@ -74,12 +74,17 @@
74
74
  }}
75
75
  </v-alert>
76
76
 
77
- <ContentBlockAsset
78
- v-if="sourceInherit && hasLinkedCaptions"
79
- :value="linkedCaptions"
80
- :assets="assets"
81
- disabled
82
- ></ContentBlockAsset>
77
+ <!-- Display ALL linked captions when inheriting -->
78
+ <div v-if="sourceInherit && hasLinkedCaptions">
79
+ <div v-for="(caption, index) in linkedCaptions" :key="caption.file_asset_id || index" class="mb-2">
80
+ <ContentBlockAsset
81
+ :value="caption"
82
+ :assets="assets"
83
+ :label="getCaptionLabel(caption)"
84
+ disabled
85
+ ></ContentBlockAsset>
86
+ </div>
87
+ </div>
83
88
 
84
89
  <ContentBlockAsset
85
90
  v-if="!sourceInherit"
@@ -169,19 +174,12 @@ export default {
169
174
  data() {
170
175
  return {
171
176
  sourceInherit: true,
172
- linkedCaptions: null,
177
+ linkedCaptions: [],
173
178
  }
174
179
  },
175
180
  computed: {
176
181
  hasLinkedCaptions() {
177
- return this.linkedCaptions !== null
178
- },
179
- linkedCaptionsName() {
180
- return _.get(
181
- this.linkedCaptions,
182
- 'asset.name',
183
- _.get(this.linkedCaptions, 'asset.public_url', '???')
184
- )
182
+ return this.linkedCaptions && this.linkedCaptions.length > 0
185
183
  },
186
184
  isFromUrl() {
187
185
  if (
@@ -225,7 +223,7 @@ export default {
225
223
  methods: {
226
224
  onSourceChange(file, rawFile) {
227
225
  this.linkedCaptions = this.getLinkedCaptions(rawFile)
228
- this.sourceInherit = this.linkedCaptions !== null
226
+ this.sourceInherit = this.hasLinkedCaptions
229
227
 
230
228
  this.$emit('change:source', file, rawFile)
231
229
  },
@@ -238,26 +236,36 @@ export default {
238
236
  onUpdateAssets(assets) {
239
237
  this.$emit('update:assets', assets)
240
238
  },
239
+ /**
240
+ * Get ALL linked captions (not just the first one)
241
+ * @param {Object} file - The video file object
242
+ * @returns {Array} Array of VTT caption files
243
+ */
241
244
  getLinkedCaptions(file) {
242
245
  if (!file) {
243
- return null
246
+ return []
244
247
  }
245
- // Check to see if the video source has a linked asset and it's a vtt file.
246
- // Prefer the most recently linked VTT (last match).
247
- const linkedAssets = _.get(file, 'asset.linked_assets', [])
248
- const linkedCaption = _.findLast(linkedAssets, function (f) {
249
- const ext = _.get(f, 'asset.metadata.extension', '').toLowerCase()
250
- const mime = _.get(f, 'asset.metadata.mime', '').toLowerCase()
251
- const name = _.get(f, 'asset.name', '').toLowerCase()
252
-
253
- return (
254
- ext === 'vtt' ||
255
- mime === 'text/vtt' ||
256
- name.endsWith('.vtt')
257
- )
258
- })
248
+ // Get ALL VTT files from linked_assets (not just the first one)
249
+ const linkedCaptions = _.filter(
250
+ _.get(file, 'asset.linked_assets', []),
251
+ function (f) {
252
+ return _.get(f, 'asset.metadata.extension', '') === 'vtt'
253
+ }
254
+ )
259
255
 
260
- return linkedCaption || null
256
+ return linkedCaptions || []
257
+ },
258
+ /**
259
+ * Get the label with locale in parentheses for a caption
260
+ * Format: "(EN-US)" - if no locale, assume English
261
+ * @param {Object} caption - The caption file object
262
+ * @returns {string} Label with locale code
263
+ */
264
+ getCaptionLabel(caption) {
265
+ const localeCode = _.get(caption, 'locale.code') ||
266
+ _.get(caption, 'asset.metadata.locale') ||
267
+ 'EN-US'
268
+ return `(${localeCode})`
261
269
  },
262
270
  },
263
271
  }
@@ -2,7 +2,8 @@
2
2
  <component
3
3
  :is="convertedContent"
4
4
  v-show="value"
5
- :class="textViewer ? 'text-viewer' : ''"
5
+ :class="contentClasses"
6
+ :dir="textDirection"
6
7
  ></component>
7
8
  </template>
8
9
 
@@ -28,7 +29,28 @@ export default {
28
29
  ...mapGetters({
29
30
  course: 'course/get',
30
31
  glossaryTerms: 'glossary/getTerms',
32
+ isRtl: 'course/isRtl',
33
+ courseTextDirection: 'course/textDirection',
31
34
  }),
35
+ /**
36
+ * Get the text direction based on course locale
37
+ * @returns {string} 'rtl' or 'ltr'
38
+ */
39
+ textDirection() {
40
+ return this.courseTextDirection || 'ltr'
41
+ },
42
+ /**
43
+ * Generate CSS classes for the content container
44
+ * Includes RTL-specific classes when course is RTL
45
+ * @returns {Object} Object of CSS class names
46
+ */
47
+ contentClasses() {
48
+ return {
49
+ 'text-viewer': this.textViewer,
50
+ 'rtl-content': this.isRtl,
51
+ 'ltr-content': !this.isRtl,
52
+ }
53
+ },
32
54
  convertedContent() {
33
55
  let content = ''
34
56
  if (this.value) {
@@ -96,4 +118,161 @@ caption {
96
118
  color: var(--v-primary-base);
97
119
  text-align: left;
98
120
  }
121
+
122
+ /**
123
+ * RTL (Right-to-Left) Language Support Styles
124
+ * Applied when content is in RTL languages (Arabic, Hebrew, Persian, Urdu)
125
+ *
126
+ * Uses unicode-bidi: isolate to create a directional isolation boundary.
127
+ * This ensures RTL content displays correctly even when system UI is LTR.
128
+ */
129
+ .rtl-content {
130
+ /* Base RTL text properties */
131
+ direction: rtl;
132
+ text-align: right;
133
+ unicode-bidi: isolate;
134
+
135
+ /* Paragraph and text alignment */
136
+ p,
137
+ h1,
138
+ h2,
139
+ h3,
140
+ h4,
141
+ h5,
142
+ h6,
143
+ li,
144
+ td,
145
+ th,
146
+ label,
147
+ span,
148
+ div {
149
+ text-align: right;
150
+ }
151
+
152
+ /* Lists - reverse bullet/number position */
153
+ ul,
154
+ ol {
155
+ padding-right: 40px;
156
+ padding-left: 0;
157
+ }
158
+
159
+ li {
160
+ text-align: right;
161
+ }
162
+
163
+ /* Table alignment */
164
+ table {
165
+ direction: rtl;
166
+ }
167
+
168
+ th,
169
+ td {
170
+ text-align: right;
171
+ }
172
+
173
+ /* Caption alignment for RTL */
174
+ caption {
175
+ text-align: right;
176
+ }
177
+
178
+ /* Blockquote RTL styling */
179
+ blockquote {
180
+ border-right: 4px solid var(--v-primary-base);
181
+ border-left: none;
182
+ padding-right: 16px;
183
+ padding-left: 0;
184
+ margin-right: 0;
185
+ margin-left: 0;
186
+ }
187
+
188
+ /* Handle mixed LTR content within RTL */
189
+ /* Use explicit dir="ltr" on elements that need left-to-right */
190
+ [dir='ltr'] {
191
+ direction: ltr;
192
+ text-align: left;
193
+ }
194
+
195
+ /* Code blocks should remain LTR */
196
+ pre,
197
+ code {
198
+ direction: ltr;
199
+ text-align: left;
200
+ }
201
+
202
+ /* Images - keep centered or as specified */
203
+ img {
204
+ /* Images are typically direction-neutral */
205
+ }
206
+
207
+ /* Form elements */
208
+ input,
209
+ textarea,
210
+ select {
211
+ direction: rtl;
212
+ text-align: right;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * LTR (Left-to-Right) Language Support Styles
218
+ * Applied when content is in LTR languages (English, Spanish, French, etc.)
219
+ *
220
+ * Uses unicode-bidi: isolate to create a directional isolation boundary.
221
+ * This ensures LTR content displays correctly even when system UI is RTL.
222
+ */
223
+ .ltr-content {
224
+ direction: ltr;
225
+ text-align: left;
226
+ unicode-bidi: isolate;
227
+
228
+ /* Paragraph and text alignment */
229
+ p,
230
+ h1,
231
+ h2,
232
+ h3,
233
+ h4,
234
+ h5,
235
+ h6,
236
+ li,
237
+ td,
238
+ th,
239
+ label,
240
+ span,
241
+ div {
242
+ text-align: left;
243
+ }
244
+
245
+ /* Lists - standard bullet/number position */
246
+ ul,
247
+ ol {
248
+ padding-left: 40px;
249
+ padding-right: 0;
250
+ }
251
+
252
+ caption {
253
+ text-align: left;
254
+ }
255
+
256
+ /* Handle mixed RTL content within LTR */
257
+ [dir='rtl'] {
258
+ direction: rtl;
259
+ text-align: right;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * RTL-aware responsive styles
265
+ */
266
+ @media (max-width: 600px) {
267
+ .rtl-content {
268
+ /* Maintain RTL on mobile */
269
+ text-align: right;
270
+
271
+ ul,
272
+ ol {
273
+ padding-right: 20px;
274
+ padding-left: 0;
275
+ }
276
+ }
277
+ }
99
278
  </style>
@@ -156,6 +156,7 @@ export default {
156
156
  showGlossary: { type: Boolean, required: false, default: false },
157
157
  render: { type: Boolean, required: false, default: false },
158
158
  hideTextEditor: { type: Boolean, required: false, default: false },
159
+ defaultAlignment: { type: String, required: false, default: null },
159
160
  },
160
161
  data() {
161
162
  return {
@@ -165,7 +166,13 @@ export default {
165
166
  paused: false,
166
167
  isRevising: false,
167
168
  rephraseToneIndex: 0,
168
- toneSequence: ['neutral', 'conversational', 'formal', 'succinct', 'encouraging'],
169
+ toneSequence: [
170
+ 'neutral',
171
+ 'conversational',
172
+ 'formal',
173
+ 'succinct',
174
+ 'encouraging',
175
+ ],
169
176
  }
170
177
  },
171
178
 
@@ -356,6 +363,15 @@ export default {
356
363
  value: 'windward-table-subject-report',
357
364
  },
358
365
  ],
366
+ init_instance_callback: (editor) => {
367
+ if (this.defaultAlignment) {
368
+ editor.execCommand(
369
+ 'mceToggleFormat',
370
+ false,
371
+ this.defaultAlignment
372
+ )
373
+ }
374
+ },
359
375
  setup: () => {
360
376
  // Here we can add plugin
361
377
  getTinymce().PluginManager.add(
@@ -579,9 +595,10 @@ export default {
579
595
  return null
580
596
  }
581
597
 
582
- const tone = this.toneSequence[
583
- this.rephraseToneIndex % this.toneSequence.length
584
- ]
598
+ const tone =
599
+ this.toneSequence[
600
+ this.rephraseToneIndex % this.toneSequence.length
601
+ ]
585
602
  this.rephraseToneIndex =
586
603
  (this.rephraseToneIndex + 1) % this.toneSequence.length
587
604
 
@@ -748,12 +765,12 @@ export default {
748
765
  }
749
766
 
750
767
  // Wrap response with temporary markers so we can reselect inserted content
751
- const startId = `ww-revise-start-${this.seed}-${Date.now()}-${Math.random()
752
- .toString(36)
753
- .slice(2)}`
754
- const endId = `ww-revise-end-${this.seed}-${Date.now()}-${Math.random()
755
- .toString(36)
756
- .slice(2)}`
768
+ const startId = `ww-revise-start-${
769
+ this.seed
770
+ }-${Date.now()}-${Math.random().toString(36).slice(2)}`
771
+ const endId = `ww-revise-end-${
772
+ this.seed
773
+ }-${Date.now()}-${Math.random().toString(36).slice(2)}`
757
774
  const wrappedHtml =
758
775
  `<span id="${startId}" data-ww-revise="s"></span>` +
759
776
  responseData.html +
@@ -769,7 +786,10 @@ export default {
769
786
  if (startEl && endEl) {
770
787
  const selectRange = editor.dom.createRng()
771
788
  // Select everything between markers
772
- if (selectRange.setStartAfter && selectRange.setEndBefore) {
789
+ if (
790
+ selectRange.setStartAfter &&
791
+ selectRange.setEndBefore
792
+ ) {
773
793
  selectRange.setStartAfter(startEl)
774
794
  selectRange.setEndBefore(endEl)
775
795
  } else {
@@ -787,15 +807,19 @@ export default {
787
807
  }
788
808
  const startIndex = childIndex(startEl) + 1
789
809
  const endIndex = childIndex(endEl)
790
- selectRange.setStart(startParent, startIndex)
810
+ selectRange.setStart(
811
+ startParent,
812
+ startIndex
813
+ )
791
814
  selectRange.setEnd(endParent, endIndex)
792
815
  }
793
816
  editor.selection.setRng(selectRange)
794
817
 
795
818
  // Remove markers after selection is set
796
- if (startEl.parentNode) startEl.parentNode.removeChild(startEl)
797
- if (endEl.parentNode) endEl.parentNode.removeChild(endEl)
798
-
819
+ if (startEl.parentNode)
820
+ startEl.parentNode.removeChild(startEl)
821
+ if (endEl.parentNode)
822
+ endEl.parentNode.removeChild(endEl)
799
823
  }
800
824
  } catch (_e) {
801
825
  // Ignore selection restoration errors
@@ -2,4 +2,22 @@ export default {
2
2
  initial_setup: 'Add question text to get started.',
3
3
  your_response: 'Your Response',
4
4
  sample_response: 'Suggested/Sample Response',
5
+ ai_feedback: 'AI Feedback',
6
+ ai_feedback_generating: 'Generating feedback…',
7
+ ai_feedback_retry: 'Retry',
8
+ ai_feedback_sources: 'Suggested pages to review',
9
+ ai_feedback_not_available: 'No AI feedback available yet.',
10
+ ai_feedback_error:
11
+ 'We could not generate AI feedback right now. Please try again.',
12
+ ai_feedback_timeout:
13
+ 'AI feedback is taking longer than expected. Please try again.',
14
+ untitled_page: 'Untitled page',
15
+ understanding_level: 'Understanding level',
16
+ understanding_levels: {
17
+ strong: 'Strong Understanding',
18
+ clear: 'Clear Understanding',
19
+ developing: 'Developing Understanding',
20
+ emerging: 'Emerging Understanding',
21
+ limited: 'Limited Understanding',
22
+ },
5
23
  }
@@ -1,8 +1,13 @@
1
1
  export default {
2
2
  question: 'Question',
3
3
  sample_response: 'Suggested/Sample Response',
4
+ ai_mode_for_student: 'AI mode for student',
4
5
  starting_text: 'Starting Text',
5
6
  title: 'Open Response',
6
7
  instructions:
7
8
  'Type your answer in the text box and click "Submit" to save your response.',
9
+ validation: {
10
+ sample_response_required_ai_mode:
11
+ 'Suggested/Sample Response is required when AI mode for student is enabled.',
12
+ },
8
13
  }
@@ -2,4 +2,22 @@ export default {
2
2
  initial_setup: 'Agrega el texto de la pregunta para comenzar.',
3
3
  your_response: 'Tu respuesta',
4
4
  sample_response: 'Respuesta sugerida/de muestra',
5
+ ai_feedback: 'Retroalimentación de IA',
6
+ ai_feedback_generating: 'Generando retroalimentación…',
7
+ ai_feedback_retry: 'Reintentar',
8
+ ai_feedback_sources: 'Páginas sugeridas para revisar',
9
+ ai_feedback_not_available: 'Aún no hay retroalimentación de IA disponible.',
10
+ ai_feedback_error:
11
+ 'No pudimos generar la retroalimentación de IA en este momento. Por favor, vuelva a intentarlo.',
12
+ ai_feedback_timeout:
13
+ 'La retroalimentación de IA está tardando más de lo esperado. Por favor, vuelva a intentarlo.',
14
+ untitled_page: 'Página sin título',
15
+ understanding_level: 'Nivel de comprensión',
16
+ understanding_levels: {
17
+ strong: 'Comprensión sólida',
18
+ clear: 'Comprensión clara',
19
+ developing: 'Comprensión en desarrollo',
20
+ emerging: 'Comprensión emergente',
21
+ limited: 'Comprensión limitada',
22
+ },
5
23
  }
@@ -1,8 +1,13 @@
1
1
  export default {
2
2
  question: 'Pregunta',
3
3
  sample_response: 'Respuesta sugerida/de muestra',
4
+ ai_mode_for_student: 'Modo de IA para el estudiante',
4
5
  starting_text: 'Texto inicial',
5
6
  title: 'Respuesta abierta',
6
7
  instructions:
7
8
  'Escriba su respuesta en el cuadro de texto y haga clic en "Enviar" para guardar su respuesta.',
9
+ validation: {
10
+ sample_response_required_ai_mode:
11
+ 'La respuesta sugerida/de muestra es obligatoria cuando el modo de IA para el estudiante está habilitado.',
12
+ },
8
13
  }
@@ -2,4 +2,22 @@ export default {
2
2
  initial_setup: 'Lägg till frågetext för att komma igång.',
3
3
  your_response: 'Ditt svar',
4
4
  sample_response: 'Föreslagen/provsvar',
5
+ ai_feedback: 'AI-feedback',
6
+ ai_feedback_generating: 'Genererar feedback…',
7
+ ai_feedback_retry: 'Försök igen',
8
+ ai_feedback_sources: 'Föreslagna sidor att granska',
9
+ ai_feedback_not_available: 'Ingen AI-feedback tillgänglig ännu.',
10
+ ai_feedback_error:
11
+ 'Vi kunde inte generera AI-feedback just nu. Försök igen.',
12
+ ai_feedback_timeout:
13
+ 'AI-feedback tar längre tid än förväntat. Försök igen.',
14
+ untitled_page: 'Sida utan titel',
15
+ understanding_level: 'Förståelsenivå',
16
+ understanding_levels: {
17
+ strong: 'Stark förståelse',
18
+ clear: 'Tydlig förståelse',
19
+ developing: 'Förståelse under utveckling',
20
+ emerging: 'Begynnande förståelse',
21
+ limited: 'Begränsad förståelse',
22
+ },
5
23
  }
@@ -1,8 +1,13 @@
1
1
  export default {
2
2
  question: 'Fråga',
3
3
  sample_response: 'Föreslagen/provsvar',
4
+ ai_mode_for_student: 'AI-läge för studenten',
4
5
  starting_text: 'Starttext',
5
6
  title: 'Öppet svar',
6
7
  instructions:
7
8
  'Skriv ditt svar i textrutan och klicka på "Skicka" för att spara ditt svar.',
9
+ validation: {
10
+ sample_response_required_ai_mode:
11
+ 'Föreslagen/provsvar krävs när AI-läge för studenten är aktiverat.',
12
+ },
8
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/core",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Windward UI Core Plugins",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "homepage": "https://bitbucket.org/mindedge/windward-ui-plugin-core#readme",
23
23
  "dependencies": {
24
- "@mindedge/vuetify-player": "^0.5.1",
24
+ "@mindedge/vuetify-player": "^0.5.2",
25
25
  "@tinymce/tinymce-vue": "^3.2.8",
26
26
  "accessibility-scanner": "^0.0.1",
27
27
  "eslint": "^8.11.0",
@@ -28,7 +28,7 @@ describe('ClickableIconsSettings', () => {
28
28
  wrapper.vm.onAddElement()
29
29
  expect(wrapper.vm.$data.block.metadata.config.items).toEqual([
30
30
  {
31
- icon: '',
31
+ icon: 'mdi-star',
32
32
  fileConfig: {},
33
33
  iconImage: false,
34
34
  title: '',