From 21f0c5cdb9175f63457cb900f9c9a93fde5ce0c5 Mon Sep 17 00:00:00 2001 From: Caio Kinzel Filho Date: Sun, 14 Dec 2025 19:37:36 +1000 Subject: [PATCH] feat(xp): add interactive code-editor ui for experiences Replace the vertical timeline with a file-explorer and code-editor styled view for career experiences to provide a more interactive, developer-oriented presentation. This change adds a sidebar file-tree, editor tabs, breadcrumb, line-numbered code block rendering of each experience (including JSDoc-style comments, export object, tags and stack), a logo watermark, and an editor status bar. It also introduces state for active tabs, tab closing, and adjustments to utility functions and styles (spacing, responsive tweaks, syntax classes) to support the new UI layout. --- src/pages/ExperiencePage.vue | 955 ++++++++++++++++++++++++----------- 1 file changed, 668 insertions(+), 287 deletions(-) diff --git a/src/pages/ExperiencePage.vue b/src/pages/ExperiencePage.vue index a3982e9..6d51d79 100644 --- a/src/pages/ExperiencePage.vue +++ b/src/pages/ExperiencePage.vue @@ -7,89 +7,389 @@ + + +
+ +
+ 📁 + career/ +
+ + +
-
-
-
-
-
- -
-
- -
+
+ +
+
+ + + +
+
+ +
+
+ + +
+ career + / + {{ visibleExperiences[activeTab]?.slug }}.ts +
+ + +
+
+ +
+ 1 + import + { + Experience + } + from + "@career/types";
-
-

- - {{ experience.company }} - - {{ experience.company }} -

- - {{ experience.position }} - • via {{ getViaName(experience.via) }} - + + +
+ 2 + +
+ + +
+ 3 + /** +
+
+ 4 + + * {{ visibleExperiences[activeTab].position }} @ + {{ visibleExperiences[activeTab].company }}
-
-
- {{ formatDateRange(experience) }} - {{ calculateDuration(experience) }} +
+ {{ 5 + hIndex }} + * - {{ highlight }} +
+
+ {{ + 5 + visibleExperiences[activeTab].highlights.length + }} + + */ +
+ + +
+ {{ + 6 + visibleExperiences[activeTab].highlights.length + }} + export const + experience: + Experience + = + { +
+ + +
+ {{ + 7 + visibleExperiences[activeTab].highlights.length + }} + company: + "{{ visibleExperiences[activeTab].company }}", +
+ + +
+ {{ + 8 + visibleExperiences[activeTab].highlights.length + }} + position: + "{{ visibleExperiences[activeTab].position }}", +
+ + + + + +
+ {{ + (visibleExperiences[activeTab].via ? 10 : 9) + + visibleExperiences[activeTab].highlights.length + }} + period: + { +
+
+ {{ + (visibleExperiences[activeTab].via ? 11 : 10) + + visibleExperiences[activeTab].highlights.length + }} + start: + new + Date("{{ visibleExperiences[activeTab].startDate }}"), +
+
+ {{ + (visibleExperiences[activeTab].via ? 12 : 11) + + visibleExperiences[activeTab].highlights.length + }} + end: + + +
+
+ {{ + (visibleExperiences[activeTab].via ? 13 : 12) + + visibleExperiences[activeTab].highlights.length + }} + duration: + "{{ calculateDuration(visibleExperiences[activeTab]) }}", +
+
+ {{ + (visibleExperiences[activeTab].via ? 14 : 13) + + visibleExperiences[activeTab].highlights.length + }} + },
-
-
    -
  • +
    + {{ + (visibleExperiences[activeTab].via ? 15 : 14) + + visibleExperiences[activeTab].highlights.length + }} + tags: + ["{{ tag }}", + ], +
    + + +
    + {{ + (visibleExperiences[activeTab].via ? 16 : 15) + + visibleExperiences[activeTab].highlights.length + }} + stack: + [ +
    +
    - {{ highlight }} -
  • -
- -
- {{ + (visibleExperiences[activeTab].via ? 17 : 16) + + visibleExperiences[activeTab].highlights.length + + techIndex + }} + "{{ tech }}", +
+
+ {{ + (visibleExperiences[activeTab].via ? 17 : 16) + + visibleExperiences[activeTab].highlights.length + + parseTechnologies(visibleExperiences[activeTab].technologies).length + }} + ], +
+ + +
+ {{ + (visibleExperiences[activeTab].via ? 18 : 17) + + visibleExperiences[activeTab].highlights.length + + parseTechnologies(visibleExperiences[activeTab].technologies).length + }} + website: + "{{ visibleExperiences[activeTab].website }}", +
+ + +
+ {{ + (visibleExperiences[activeTab].via ? 19 : 18) + + visibleExperiences[activeTab].highlights.length + + parseTechnologies(visibleExperiences[activeTab].technologies).length + + (visibleExperiences[activeTab].website ? 1 : 0) + }} + }; +
+
+ + +
+ - - - -
-
+ + +
+
+ TypeScript + UTF-8 +
+
+ Ln {{ 1 }}, Col {{ 1 }} - + Spaces: 2 +
@@ -119,36 +419,18 @@ return companyLogos[experience.slug]; } - // Via company logos and names mapping - const viaLogos: Record = { - toptal: companyLogos["toptal"], - tw: companyLogos["thoughtworks"], - }; - const viaNames: Record = { toptal: "Toptal", tw: "ThoughtWorks", }; - function getViaLogo(via: string): string | undefined { - return viaLogos[via]; - } - function getViaName(via: string): string { return viaNames[via] || via; } const INITIAL_VISIBLE_COUNT = 6; const showAll = ref(false); - - const yearsOfExperience = computed(() => { - const oldestStartDate = experiencesConfig - .map((exp) => new Date(exp.startDate).getTime()) - .sort((a, b) => a - b)[0]; - const startYear = new Date(oldestStartDate).getFullYear(); - const currentYear = new Date().getFullYear(); - return currentYear - startYear; - }); + const activeTab = ref(0); const visibleExperiences = computed(() => { if (showAll.value) { @@ -157,6 +439,13 @@ return experiencesConfig.slice(0, INITIAL_VISIBLE_COUNT); }); + function closeTab(index: number) { + if (visibleExperiences.value.length <= 1) return; + if (activeTab.value >= index && activeTab.value > 0) { + activeTab.value--; + } + } + const RECENT_YEARS = 5; const recentTechnologies = computed(() => { @@ -172,45 +461,16 @@ } }); - return Array.from(techSet).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return Array.from(techSet).sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()) + ); }); - function formatDateRange(experience: Experience): string { - const startDate = new Date(experience.startDate); - const startStr = formatMonth(startDate); - - if (!experience.endDate) { - return `${startStr} - present`; - } - - const endDate = new Date(experience.endDate); - const endStr = formatMonth(endDate); - return `${startStr} - ${endStr}`; - } - - function formatMonth(date: Date): string { - const months = [ - "jan", - "feb", - "mar", - "apr", - "may", - "jun", - "jul", - "aug", - "sep", - "oct", - "nov", - "dec", - ]; - return `${months[date.getMonth()]}/${date.getFullYear()}`; - } - function calculateDuration(experience: Experience): string { const startDate = new Date(experience.startDate); const endDate = experience.endDate ? new Date(experience.endDate) : new Date(); - let months = + const months = (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth()); @@ -219,20 +479,16 @@ let duration = ""; if (years > 0) { - duration += `${years} ${years === 1 ? "year" : "years"}`; + duration += `${years} year${years > 1 ? "s" : ""}`; } if (remainingMonths > 0) { if (duration) duration += ", "; - duration += `${remainingMonths} ${remainingMonths === 1 ? "month" : "months"}`; + duration += `${remainingMonths} month${remainingMonths > 1 ? "s" : ""}`; } if (!duration) { duration = "< 1 month"; } - if (!experience.endDate) { - duration += " (and counting)"; - } - return duration; } @@ -249,11 +505,20 @@ .page-layout { display: flex; - gap: var(--space-8); + gap: var(--space-6); max-width: 1200px; margin: 0 auto; } + .page-title { + margin-bottom: var(--space-8); + text-align: center; + max-width: 1200px; + margin-left: auto; + margin-right: auto; + } + + /* Sidebar */ .sidebar { position: sticky; top: calc(56px + var(--space-8)); @@ -283,217 +548,346 @@ margin: 0 0 var(--space-4) 0; } - .content { - flex: 1; - max-width: var(--content-max-width); + /* File tree */ + .file-tree { + margin-top: var(--space-6); + padding-top: var(--space-4); + border-top: 1px solid var(--color-border); } - .page-title { - margin-bottom: var(--space-8); - text-align: center; - max-width: 1200px; - margin-left: auto; - margin-right: auto; + .tree-folder { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-1) 0; + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--color-text-primary); + } + + .folder-icon { + font-size: 14px; } - .timeline { + .folder-name { + font-weight: var(--font-semibold); + } + + .tree-file { display: flex; - flex-direction: column; - gap: var(--space-8); + align-items: center; + gap: var(--space-2); + width: 100%; + padding: var(--space-1) var(--space-1) var(--space-1) var(--space-4); + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--color-text-secondary); + background: none; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + text-align: left; + transition: background-color var(--transition-fast); } - .experience-card { - background: var(--color-surface); + .tree-file:hover { + background: var(--color-surface-hover); + } + + .tree-file.active { + background: var(--color-primary-light); + color: var(--color-primary); + } + + .tree-file.more-files { + color: var(--color-text-muted); + font-style: italic; + } + + .file-icon { + font-size: 12px; + opacity: 0.7; + } + + .file-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + /* Content */ + .content { + flex: 1; + min-width: 0; + } + + /* Code editor */ + .code-editor { + background: var(--color-background-elevated); + border: 1px solid var(--color-border); border-radius: var(--radius-lg); - padding: var(--space-6); - box-shadow: var(--shadow-md); - transition: background-color var(--transition-theme), box-shadow var(--transition-theme); + overflow: hidden; + box-shadow: var(--shadow-lg); } - .experience-header { + .editor-header { display: flex; - justify-content: space-between; - align-items: flex-start; - gap: var(--space-4); - margin-bottom: var(--space-4); - padding-bottom: var(--space-4); + align-items: center; + background: var(--color-surface); border-bottom: 1px solid var(--color-border); } - .logos-wrapper { + .window-controls { display: flex; - align-items: center; - gap: var(--space-2); + gap: 8px; + padding: var(--space-3) var(--space-4); flex-shrink: 0; } - .company-logo-wrapper { - flex-shrink: 0; + .control { + width: 12px; + height: 12px; + border-radius: var(--radius-full); + background: var(--color-border); } - .company-logo { - width: 58px; - height: 58px; - border-radius: var(--radius-md); - object-fit: cover; + .control.close { + background: #ff5f57; } - .via-logo-wrapper { - flex-shrink: 0; + .control.minimize { + background: #febc2e; } - .via-logo { - width: 32px; - height: 32px; - border-radius: var(--radius-sm); - object-fit: cover; - opacity: 0.8; + .control.maximize { + background: #28c840; } - .experience-title { + .editor-tabs { display: flex; - flex-direction: column; - gap: var(--space-1); flex: 1; + overflow-x: auto; + scrollbar-width: none; } - .company-name { + .editor-tabs::-webkit-scrollbar { + display: none; + } + + .tab { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); font-family: var(--font-mono); - font-size: var(--text-xl); - font-weight: var(--font-semibold); - margin: 0; + font-size: var(--text-xs); + color: var(--color-text-muted); + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: + color var(--transition-fast), + background-color var(--transition-fast); + } + + .tab:hover { + color: var(--color-text-secondary); + background: var(--color-surface-hover); + } + + .tab.active { color: var(--color-text-primary); - text-transform: lowercase; + background: var(--color-background-elevated); + border-bottom-color: var(--color-primary); } - .company-name::before { - content: "// "; - color: var(--color-primary); + .tab-icon { + font-size: 10px; + padding: 1px 3px; + background: var(--color-primary); + color: white; + border-radius: 2px; + font-weight: var(--font-bold); } - .company-link { - color: inherit; - text-decoration: none; - transition: color var(--transition-fast); + .tab-name { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; } - .company-link:hover { - color: var(--color-primary); + .tab-close { + opacity: 0; + font-size: 14px; + line-height: 1; + padding: 0 2px; + border-radius: 2px; + transition: opacity var(--transition-fast); + } + + .tab:hover .tab-close { + opacity: 0.5; + } + + .tab-close:hover { + opacity: 1 !important; + background: var(--color-surface-active); } - .position { + .editor-breadcrumb { + display: flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-2) var(--space-4); font-family: var(--font-mono); - font-size: var(--text-base); - color: var(--color-primary); - text-transform: lowercase; - font-weight: var(--font-semibold); + font-size: var(--text-xs); + color: var(--color-text-muted); + background: var(--color-background-elevated); + border-bottom: 1px solid var(--color-border-subtle); } - .via-text { + .breadcrumb-sep { + opacity: 0.5; + } + + .breadcrumb-item.active { + color: var(--color-text-secondary); + } + + .editor-content { + position: relative; + padding: var(--space-4) 0; + min-height: 400px; + overflow-x: auto; + } + + .code-block { font-family: var(--font-mono); font-size: var(--text-sm); - color: var(--color-text-muted); - text-transform: lowercase; + line-height: 1.6; } - .experience-meta { + .line { display: flex; - flex-direction: column; - align-items: flex-end; - gap: var(--space-1); + padding: 0 var(--space-4); + min-height: 1.6em; + } + + .line:hover { + background: var(--color-surface-hover); + } + + .line-number { + color: var(--color-text-muted); + opacity: 0.5; + min-width: 40px; text-align: right; + padding-right: var(--space-4); + user-select: none; + flex-shrink: 0; } - .date-info { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: var(--space-1); + .line code { + white-space: pre-wrap; + word-break: break-word; } - .date-range { - font-family: var(--font-mono); - font-size: var(--text-sm); - color: var(--color-text-secondary); - text-transform: lowercase; + .line code.indent-1 { + padding-left: 2ch; } - .duration { - font-family: var(--font-mono); - font-size: var(--text-xs); + .line code.indent-2 { + padding-left: 4ch; + } + + /* Syntax highlighting */ + .code-keyword { + color: var(--color-purple); + } + + .code-type { + color: var(--color-teal); + } + + .code-var { + color: var(--quadrant-NE); + } + + .code-prop { + color: var(--quadrant-NE); + } + + .code-string { + color: var(--color-green); + } + + .code-punct { color: var(--color-text-muted); } - .highlights { - list-style: none; - padding: 0; - margin: 0 0 var(--space-4) 0; - display: flex; - flex-direction: column; - gap: var(--space-2); + .code-comment { + color: var(--color-text-muted); + font-style: italic; } - .highlight-item { - font-family: var(--font-sans); - font-size: var(--text-base); - color: var(--color-text-secondary); - line-height: var(--leading-relaxed); - padding-left: var(--space-4); - position: relative; + .code-link { + color: inherit; + text-decoration: underline; + text-decoration-style: dotted; } - .highlight-item::before { - content: ">"; - position: absolute; - left: 0; + .code-link:hover { color: var(--color-primary); - font-family: var(--font-mono); - font-weight: var(--font-bold); } - .experience-footer { - padding-top: var(--space-4); - border-top: 1px solid var(--color-border); + /* Logo watermark */ + .logo-watermark { + position: absolute; + bottom: var(--space-4); + right: var(--space-4); + opacity: 0.08; + pointer-events: none; } - .show-more-container { - display: flex; - justify-content: center; - padding: var(--space-6) 0; + .logo-watermark img { + width: 120px; + height: 120px; + border-radius: var(--radius-lg); + object-fit: cover; } - .show-more-button { + /* Status bar */ + .editor-statusbar { display: flex; - flex-direction: column; - align-items: flex-start; - gap: var(--space-1); + justify-content: space-between; + padding: var(--space-1) var(--space-4); + background: var(--color-primary); font-family: var(--font-mono); - font-size: var(--text-sm); - color: var(--color-text-secondary); - background: none; - border: none; - cursor: pointer; - padding: var(--space-4); - text-transform: lowercase; - transition: color var(--transition-fast); - } - - .show-more-button:hover { - color: var(--color-text-primary); + font-size: var(--text-xs); + color: white; } - .show-more-button .comment-prefix { - color: var(--color-primary); + .status-left, + .status-right { + display: flex; + gap: var(--space-4); } - .show-more-line { - display: block; + .status-item { + opacity: 0.9; } + /* Responsive */ @media (--lg) { .sidebar { width: 180px; } + + .tab-name { + max-width: 80px; + } } @media (--md) { @@ -510,42 +904,29 @@ width: 100%; } - .experience-header { - flex-wrap: wrap; - gap: var(--space-3); - } - - .company-logo { - width: 50px; - height: 50px; - } - - .via-logo { - width: 26px; - height: 26px; + .file-tree { + display: none; } - .experience-meta { - align-items: flex-start; - text-align: left; + .window-controls { + display: none; } - .date-info { - flex-direction: row; - align-items: baseline; - gap: var(--space-3); + .tab-name { + max-width: 60px; } - .duration::before { - content: "• "; + .line-number { + min-width: 30px; + padding-right: var(--space-2); } - .company-name { - font-size: var(--text-lg); + .code-block { + font-size: var(--text-xs); } - .experience-card { - padding: var(--space-4); + .logo-watermark { + display: none; } }