diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 4f500446e..e78602b74 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -9,6 +9,7 @@ import { handlers as mswHandlers } from './mswHandlers' import 'dripicons/webfont/webfont.css' import '../src/assets/legacy/bootstrap-impresso-theme.css' import '../src/assets/legacy/bootstrap-vue.css' +import '../src/styles/style.css' /* * Initializes MSW diff --git a/src/components/CollapsiblePanel.vue b/src/components/CollapsiblePanel.vue new file mode 100644 index 000000000..876c0514b --- /dev/null +++ b/src/components/CollapsiblePanel.vue @@ -0,0 +1,124 @@ + + + + diff --git a/src/components/TutorialMonitor.vue b/src/components/TutorialMonitor.vue new file mode 100644 index 000000000..d803c11f9 --- /dev/null +++ b/src/components/TutorialMonitor.vue @@ -0,0 +1,119 @@ + + + + diff --git a/src/components/TutorialTaskPanel.vue b/src/components/TutorialTaskPanel.vue new file mode 100644 index 000000000..54f896eb6 --- /dev/null +++ b/src/components/TutorialTaskPanel.vue @@ -0,0 +1,82 @@ + + + + diff --git a/src/components/base/Icon.vue b/src/components/base/Icon.vue index 7d73b5bfa..4e3c7e11d 100644 --- a/src/components/base/Icon.vue +++ b/src/components/base/Icon.vue @@ -3,7 +3,7 @@ :width="width * scaleValue" :height="height * scaleValue" :viewBox="computedSvgViewbox" - :class="{'Icon': true }" + :class="{ Icon: true }" > = { height: 1792, paths: [ { - d: - 'M1519 776q62 0 103.5 40.5t41.5 101.5q0 97-93 130l-172 59 56 167q7 21 7 47 0 59-42 102t-101 43q-47 0-85.5-27t-53.5-72l-55-165-310 106 55 164q8 24 8 47 0 59-42 102t-102 43q-47 0-85-27t-53-72l-55-163-153 53q-29 9-50 9-61 0-101.5-40t-40.5-101q0-47 27.5-85t71.5-53l156-53-105-313-156 54q-26 8-48 8-60 0-101-40.5t-41-100.5q0-47 27.5-85t71.5-53l157-53-53-159q-8-24-8-47 0-60 42-102.5t102-42.5q47 0 85 27t53 72l54 160 310-105-54-160q-8-24-8-47 0-59 42.5-102t101.5-43q47 0 85.5 27.5t53.5 71.5l53 161 162-55q21-6 43-6 60 0 102.5 39.5t42.5 98.5q0 45-30 81.5t-74 51.5l-157 54 105 316 164-56q24-8 46-8zM725 1038l310-105-105-315-310 107z', - }, - ], + d: 'M1519 776q62 0 103.5 40.5t41.5 101.5q0 97-93 130l-172 59 56 167q7 21 7 47 0 59-42 102t-101 43q-47 0-85.5-27t-53.5-72l-55-165-310 106 55 164q8 24 8 47 0 59-42 102t-102 43q-47 0-85-27t-53-72l-55-163-153 53q-29 9-50 9-61 0-101.5-40t-40.5-101q0-47 27.5-85t71.5-53l156-53-105-313-156 54q-26 8-48 8-60 0-101-40.5t-41-100.5q0-47 27.5-85t71.5-53l157-53-53-159q-8-24-8-47 0-60 42-102.5t102-42.5q47 0 85 27t53 72l54 160 310-105-54-160q-8-24-8-47 0-59 42.5-102t101.5-43q47 0 85.5 27.5t53.5 71.5l53 161 162-55q21-6 43-6 60 0 102.5 39.5t42.5 98.5q0 45-30 81.5t-74 51.5l-157 54 105 316 164-56q24-8 46-8zM725 1038l310-105-105-315-310 107z' + } + ] }, play: { width: 24, @@ -58,10 +57,9 @@ const Icons: Record = { paths: [ { style: 'fill:white; stroke:black; stroke-width:1px;', - d: - 'M6.90588 4.53682C6.50592 4.2998 6 4.58808 6 5.05299V18.947C6 19.4119 6.50592 19.7002 6.90588 19.4632L18.629 12.5162C19.0211 12.2838 19.0211 11.7162 18.629 11.4838L6.90588 4.53682Z', - }, - ], + d: 'M6.90588 4.53682C6.50592 4.2998 6 4.58808 6 5.05299V18.947C6 19.4119 6.50592 19.7002 6.90588 19.4632L18.629 12.5162C19.0211 12.2838 19.0211 11.7162 18.629 11.4838L6.90588 4.53682Z' + } + ] }, edit: { width: 24, @@ -69,14 +67,13 @@ const Icons: Record = { paths: [ { style: 'stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round;', - d: 'M3 21L12 21H21', + d: 'M3 21L12 21H21' }, { style: 'stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round;', - d: - 'M12.2218 5.82839L15.0503 2.99996L20 7.94971L17.1716 10.7781M12.2218 5.82839L6.61522 11.435C6.42769 11.6225 6.32233 11.8769 6.32233 12.1421L6.32233 16.6776L10.8579 16.6776C11.1231 16.6776 11.3774 16.5723 11.565 16.3847L17.1716 10.7781M12.2218 5.82839L17.1716 10.7781', - }, - ], + d: 'M12.2218 5.82839L15.0503 2.99996L20 7.94971L17.1716 10.7781M12.2218 5.82839L6.61522 11.435C6.42769 11.6225 6.32233 11.8769 6.32233 12.1421L6.32233 16.6776L10.8579 16.6776C11.1231 16.6776 11.3774 16.5723 11.565 16.3847L17.1716 10.7781M12.2218 5.82839L17.1716 10.7781' + } + ] }, check: { width: 24, @@ -84,50 +81,62 @@ const Icons: Record = { paths: [ { style: 'stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round;', - d: 'M7 12.5L10 15.5L17 8.5', + d: 'M7 12.5L10 15.5L17 8.5' }, { - d: - 'M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z', - }, - ], + d: 'M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z' + } + ] }, + chevron: { + width: 24, + height: 24, + paths: [ + { + style: 'fill:transparent; stroke:black; stroke-width:1px;', + + d: 'M6 9L12 15L18 9' + } + ] + } } const props = defineProps({ name: { type: String, - default: 'slack', + default: 'slack' }, scale: { type: [Number, String], - default: 1, + default: 1 }, paths: { type: Array, - default: () => [], + default: () => [] }, polygons: { type: Array, - default: () => [], + default: () => [] }, width: { type: Number, - default: 24, + default: 24 }, height: { type: Number, - default: 24, + default: 24 }, strokeWidth: { type: Number, - default: 1, - }, + default: 1 + } }) -const computedPaths = computed(() => (props.paths.length ? props.paths as any as Path[] : Icons[props.name].paths)) +const computedPaths = computed(() => + props.paths.length ? (props.paths as any as Path[]) : Icons[props.name].paths +) const computedPolygons = computed(() => - props.polygons.length ? props.polygons as Polygon[] : Icons[props.name].polygons, + props.polygons.length ? (props.polygons as Polygon[]) : Icons[props.name].polygons ) const computedSvgViewbox = computed(() => { const { width, height } = Icons[props.name] diff --git a/src/components/stories/TutorialMonitor.stories.ts b/src/components/stories/TutorialMonitor.stories.ts new file mode 100644 index 000000000..feb8d878a --- /dev/null +++ b/src/components/stories/TutorialMonitor.stories.ts @@ -0,0 +1,96 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import TutorialMonitor from '@/components/TutorialMonitor.vue' +import { TutorialTaskModel } from '@/models/TutorialTask' + +const meta: Meta = { + title: 'Components/TutorialMonitor', + component: TutorialMonitor, + tags: ['autodocs'] +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + title: 'Tutorial Monitor Story', + isCollapsed: false, + initialOpenedTaskId: 'task-search-titanic', + tasks: [ + { + title: 'Welcome to Impresso!', + description: + 'This is your gateway to exploring 200 years of historical newspapers and radio. Don’t know where to start? Let’s get you on board!' + }, + { + title: 'Search "Titanic"', + id: 'task-search-titanic', + tasks: [ + { + title: 'Start your search by entering a keyword', + description: 'Description for subtask 1', + status: 'completed', + completionDate: new Date() + }, + { + title: 'Understanding Search results', + description: 'Description for subtask 2', + status: 'completed' + }, + { + title: 'Similar words? Use "OR"...', + description: 'Description for subtask 2', + status: 'completed' + }, + { + title: 'An overview of all similar words: word embedding', + description: 'Description for subtask 2', + status: 'completed' + }, + { + title: 'Esclude non relevant results: "NOT"', + description: 'Description for subtask 2', + status: 'completed' + }, + { + title: 'Esclude non relevant results: filter by DATE', + description: 'Description for subtask 2', + status: 'completed' + }, + { + title: 'Explore all the possible filters!' + } + ] + }, + { + title: 'Task 1', + description: 'Description for task 1', + status: 'completed', + coverUrl: 'https://via.placeholder.com/150', + tasks: [ + { + title: 'Subtask 1', + description: 'Description for subtask 1', + status: 'completed' + }, + { + title: 'Subtask 2', + description: 'Description for subtask 2', + status: 'completed' + } + ] + }, + { + title: 'Task 2', + description: 'Description for task 2', + status: 'in-progress' + }, + { + title: 'Task 3', + description: 'Description for task 3', + status: 'pending' + } + ].map((task, index) => new TutorialTaskModel({ ...task, id: task.id ?? `task-${index + 1}` })) + } +} diff --git a/src/models/TutorialTask.ts b/src/models/TutorialTask.ts new file mode 100644 index 000000000..228356a2f --- /dev/null +++ b/src/models/TutorialTask.ts @@ -0,0 +1,93 @@ +export interface ITutorialTask { + id: string + title: string + description: string + status?: string + coverUrl?: string + videoUrl?: string + creationDate?: Date + completionDate?: Date + tasks: Array +} + +export const StatusCompleted = 'completed' +export const StatusInProgress = 'in-progress' +export const StatusPending = 'pending' + +export class TutorialTaskModel implements ITutorialTask { + id: string + title: string + description: string + status: string + coverUrl: string + videoUrl: string + creationDate: Date + completionDate: Date + tasks: Array + + constructor({ + id = '', + title = '', + description = '', + status = StatusInProgress, + creationDate = new Date(), + completionDate = null, + coverUrl = '', + videoUrl = '', + tasks = [] + } = {}) { + this.id = String(id) + this.title = String(title) + this.description = String(description) + this.status = String(status) + this.coverUrl = String(coverUrl) + this.videoUrl = String(videoUrl) + + if (creationDate instanceof Date) { + this.creationDate = creationDate + } else if (creationDate) { + this.creationDate = new Date(creationDate) + } + + if (completionDate instanceof Date) { + this.completionDate = completionDate + } else if (completionDate) { + this.completionDate = new Date(completionDate) + } + this.tasks = tasks.map(task => { + if (task instanceof TutorialTaskModel) { + return task + } + return new TutorialTaskModel({ ...task }) + }) + } + + getCompletion(): { n: number; total: number; ratio: number; latestCompletionDate: Date | null } { + const totalTasks = this.tasks.length + if (totalTasks === 0) { + return { + total: 1, + n: this.status === StatusCompleted ? 1 : 0, + ratio: this.status === StatusCompleted ? 1 : 0, + latestCompletionDate: this.completionDate + } + } + const completedTasks = this.tasks.filter(task => task.status === 'completed') + const latestCompletionDate = completedTasks.reduce( + (latest, task) => { + if (!task.completionDate) { + return latest + } + return task.completionDate.getTime() > latest.getTime() ? task.completionDate : latest + }, + this.completionDate ?? new Date(0) + ) + + return { + n: completedTasks.length, + total: totalTasks, + ratio: completedTasks.length / totalTasks, + latestCompletionDate: latestCompletionDate + } + } +} diff --git a/src/stores/tutorial.ts b/src/stores/tutorial.ts new file mode 100644 index 000000000..a811ba7ce --- /dev/null +++ b/src/stores/tutorial.ts @@ -0,0 +1,8 @@ +import { defineStore } from 'pinia' + +export interface State { + tutorial: string[] +} + +export const useTutorialStore = defineStore('tutorial', { + \ No newline at end of file diff --git a/src/styles/style.css b/src/styles/style.css new file mode 100644 index 000000000..fd290e26b --- /dev/null +++ b/src/styles/style.css @@ -0,0 +1,144 @@ +@font-face { + font-family: 'Satoshi-Variable'; + src: + url('/assets/fonts/Satoshi-Variable.woff2') format('woff2'), + url('/assets/fonts/Satoshi-Variable.woff') format('woff'), + url('/assets/fonts/Satoshi-Variable.ttf') format('truetype'); + font-weight: 300 900; + font-display: swap; + font-style: normal; +} + +@font-face { + font-family: 'Satoshi-VariableItalic'; + src: + url('/assets/fonts/Satoshi-VariableItalic.woff2') format('woff2'), + url('/assets/fonts/Satoshi-VariableItalic.woff') format('woff'), + url('/assets/fonts/Satoshi-VariableItalic.ttf') format('truetype'); + font-weight: 300 900; + font-display: swap; + font-style: italic; +} + +:root { + --impresso-color-paper: #fafbf2; + + --impresso-color-yellow-code: 255, 235, 120; + --impresso-color-yellow: #ffeb78; + --impresso-color-yellow-alpha-20: rgba(255, 235, 120, 0.2); + --impresso-color-yellow-alpha-30: rgba(255, 235, 120, 0.3); + --impresso-color-yellow-alpha-50: rgba(255, 235, 120, 0.5); + --impresso-color-yellow-alpha-80: rgba(255, 235, 120, 0.8); + + --impresso-color-black: #343a40; + --impresso-color-black-rgb: 52, 58, 64; + + --impresso-color-pastel-blue: rgba(86, 204, 242); + --impresso-color-pastel-blue-alpha-20: rgba(86, 204, 242, 0.2); + + --impresso-border-radius-xs: 5px; + --impresso-border-radius-sm: 10px; + --impresso-border-radius-md: 15px; + --impresso-border-radius-lg: 20px; + --impresso-border-radius-xl: 30px; + --impresso-font-size-smallcaps: 0.72em; + --impresso-letter-spacing-smallcaps: 0.08em; + --impresso-letter-spacing-smallcaps-md: 0.12em; + --impresso-font-size-smaller: 0.85em; + --impresso-wght: 450; + --impresso-wght-bold: 700; + --impresso-wght-smallcaps: 580; + --impresso-wght-smallcaps-bold: 800; + --clr-white: #f8f8ff; + --clr-white-rgba-20: #ffffff33; + --clr-dark: var(--impresso-color-black); + + --clr-grey-100: #3d4146; + --clr-grey-200: #5b5e65; + --clr-grey-300: #84868e; + --clr-grey-400: #a9aab4; + --clr-grey-500: #c2c3cb; + --clr-grey-600: #d4d5e1; + --clr-grey-700: #e1e1ee; + --clr-grey-800: #ececfb; + --clr-grey-900: #f8f8ff; + --clr-grey-100-rgba-20: #464d5333; + --clr-grey-200-rgba-20: #585d6633; + --clr-grey-300-rgba-20: #7c7f8c33; + --clr-grey-400-rgba-20: #a0a1a333; + --clr-grey-500-rgba-20: #b1b2c633; + --clr-grey-600-rgba-20: #c3c4d933; + --clr-grey-700-rgba-20: #d5d5ec33; + --clr-grey-800-rgba-20: #e7e7ff33; + --clr-grey-900-rgba-20: #f8f8ff33; + + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 1rem; + --spacing-4: 1.5rem; + --spacing-5: 3rem; + --negative-spacing-1: -0.25rem; + --negative-spacing-2: -0.5rem; + --negative-spacing-3: -1rem; + --negative-spacing-4: -1.5rem; + --negative-spacing-5: -3rem; + + --accent: #28a745; + --impresso-yellow: #ffeb78; + --border-radius-sm: 3px; + --border-radius-md: 8px; + --border-radius-lg: 12px; + + /* Bootstrap overrides */ + --bs-font-sans-serif: 'Satoshi-Variable', sans-serif; + --bs-font-sans-serif-italic: 'Satoshi-VariableItalic', sans-serif; + --bs-font-serif-italic: 'questa', serif; + --bs-font-serif: 'questa', serif; + --bs-border-radius-lg: var(--impresso-border-radius-lg); + --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.125); + --bs-box-shadow-md: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-box-shadow-focus: 0 0 0 0.2rem #ffeb78; + --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); + + --bs-font-size-base: 1rem; + --impresso-font-size-3: 1.75rem; + --impresso-font-size-4: 1.5rem; + + --impresso-transition-ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +body { + font-family: var(--bs-font-sans-serif); + font-weight: 450; + font-variation-settings: 'wght' 450; + text-rendering: optimizeLegibility; +} + +/* impresso-specific pseudo-bootstrap implementation */ +.btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + margin: var(--spacing-2); + padding: var(--spacing-1); + border-radius: 50%; + border: 1px solid transparent; + transition: background-color 0.5s var(--impresso-transition-ease); +} +.btn-icon > path { + fill: transparent; +} +.btn-icon:hover, +.btn-icon.active { + background-color: var(--impresso-yellow); + border-color: var(--impresso-yellow); +} +.btn-icon:hover, +.btn-icon:focus { + box-shadow: var(--bs-box-shadow-focus); +} +.btn-icon.active:focus { + box-shadow: none; +}