Skip to content

Commit 9b24fa6

Browse files
committed
Add interactive features carousel to Welcome view
(#4769, #4773, PLG-138)
1 parent fadb356 commit 9b24fa6

File tree

5 files changed

+256
-13
lines changed

5 files changed

+256
-13
lines changed
Lines changed: 48 additions & 0 deletions
Loading
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { css, html, LitElement } from 'lit';
2+
import { customElement, queryAssignedElements, state } from 'lit/decorators.js';
3+
import '../../shared/components/button';
4+
import '../../shared/components/code-icon';
5+
6+
declare global {
7+
interface HTMLElementTagNameMap {
8+
'gl-feature-carousel': GlFeatureCarousel;
9+
'gl-feature-card': GlFeatureCard;
10+
}
11+
}
12+
13+
@customElement('gl-feature-carousel')
14+
export class GlFeatureCarousel extends LitElement {
15+
static override styles = [
16+
css`
17+
:host {
18+
display: block;
19+
width: 100%;
20+
}
21+
22+
.carousel {
23+
display: flex;
24+
gap: 1rem;
25+
justify-content: center;
26+
}
27+
28+
.button {
29+
display: flex;
30+
align-items: center;
31+
}
32+
33+
.content {
34+
flex: 1;
35+
max-width: 520px;
36+
display: flex;
37+
align-items: center;
38+
justify-content: center;
39+
}
40+
41+
42+
::slotted(*) {
43+
display: none;
44+
}
45+
46+
::slotted([data-active]) {
47+
display: flex;
48+
width: 100%;
49+
}
50+
`,
51+
];
52+
53+
@queryAssignedElements({ flatten: true })
54+
private cards!: HTMLElement[];
55+
56+
@state()
57+
private currentIndex = 0;
58+
59+
override firstUpdated(): void {
60+
this.updateActiveCard();
61+
}
62+
63+
private updateActiveCard(): void {
64+
this.cards.forEach((card, index) => {
65+
if (index === this.currentIndex) {
66+
card.setAttribute('data-active', '');
67+
} else {
68+
card.removeAttribute('data-active');
69+
}
70+
});
71+
}
72+
73+
private handlePrevious(): void {
74+
if (this.cards.length === 0) return;
75+
this.currentIndex = (this.currentIndex - 1 + this.cards.length) % this.cards.length;
76+
this.updateActiveCard();
77+
}
78+
79+
private handleNext(): void {
80+
if (this.cards.length === 0) return;
81+
this.currentIndex = (this.currentIndex + 1) % this.cards.length;
82+
this.updateActiveCard();
83+
}
84+
85+
private handleSlotChange(): void {
86+
this.currentIndex = 0;
87+
this.updateActiveCard();
88+
}
89+
90+
override render(): unknown {
91+
return html`
92+
<div class="carousel">
93+
<gl-button
94+
class="button"
95+
appearance="input"
96+
@click=${this.handlePrevious}
97+
aria-label="Previous feature"
98+
>
99+
<code-icon icon="chevron-left" size="20"></code-icon>
100+
</gl-button>
101+
102+
<div class="content">
103+
<slot @slotchange=${this.handleSlotChange}></slot>
104+
</div>
105+
106+
<gl-button class="button" appearance="input" @click=${this.handleNext} aria-label="Next feature">
107+
<code-icon icon="chevron-right" size="20"></code-icon>
108+
</gl-button>
109+
</div>
110+
`;
111+
}
112+
}
113+
114+
@customElement('gl-feature-card')
115+
export class GlFeatureCard extends LitElement {
116+
static override styles = [
117+
css`
118+
:host {
119+
display: flex;
120+
}
121+
122+
.image {
123+
}
124+
.content {
125+
}
126+
::slotted(img) {
127+
}
128+
129+
::slotted(h1) {
130+
}
131+
132+
::slotted(p) {
133+
}
134+
`,
135+
];
136+
137+
override render(): unknown {
138+
return html`
139+
<div class="image">
140+
<slot name="image"></slot>
141+
</div>
142+
<div class="content">
143+
<slot></slot>
144+
</div>
145+
`;
146+
}
147+
}

src/webviews/apps/welcome/welcome.css.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { css } from 'lit';
22

3-
export const welcomeStyles = css`
3+
const colorScheme = css`
4+
:host {
5+
--accent-color: #cb64ff;
6+
}
7+
`;
8+
9+
const heroGradient = css`
410
.welcome::before {
511
content: ' ';
612
position: absolute;
@@ -18,33 +24,45 @@ export const welcomeStyles = css`
1824
mix-blend-mode: color;
1925
filter: blur(53px);
2026
}
21-
.welcome__section {
27+
`;
28+
29+
const section = css`
30+
.section {
2231
display: flex;
2332
flex-flow: column;
2433
justify-content: center;
2534
align-items: center;
2635
text-align: center;
2736
}
28-
.welcome__section p {
37+
.section p {
2938
font-size: larger;
3039
max-width: calc(620px * 0.75);
3140
}
41+
`;
3242

33-
.welcome__header {
43+
const header = css`
44+
.header {
3445
margin-top: 5rem;
3546
margin-bottom: 2rem;
3647
max-width: 620px;
3748
margin-left: auto;
3849
margin-right: auto;
3950
}
40-
.welcome__header gitlens-logo {
51+
.header gitlens-logo {
4152
transform: translateX(-0.75rem);
4253
}
43-
.welcome__header h1 {
54+
.header h1 {
4455
margin-bottom: 0;
4556
}
57+
`;
4658

47-
.welcome__accent {
48-
color: #cb64ff;
59+
const typography = css`
60+
.accent {
61+
color: var(--accent-color);
4962
}
5063
`;
64+
65+
export const welcomeStyles = css`
66+
${colorScheme}
67+
${heroGradient} ${section} ${header} ${typography}
68+
`;

src/webviews/apps/welcome/welcome.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
data-placement="#{placement}"
2222
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
2323
>
24-
<gl-welcome-app name="WelcomeView" placement="#{placement}" bootstrap="#{state}"></gl-welcome-app>
24+
<gl-welcome-app
25+
name="WelcomeView"
26+
placement="#{placement}"
27+
bootstrap="#{state}"
28+
webroot="#{webroot}"
29+
></gl-welcome-app>
2530
</body>
2631
</html>

src/webviews/apps/welcome/welcome.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*global*/
22
import './welcome.scss';
33
import { html } from 'lit';
4-
import { customElement } from 'lit/decorators.js';
4+
import { customElement, property } from 'lit/decorators.js';
55
import type { State } from '../../welcome/protocol';
66
import { GlAppHost } from '../shared/appHost';
77
import { scrollableBase } from '../shared/components/styles/lit/base.css';
@@ -10,6 +10,7 @@ import type { HostIpc } from '../shared/ipc';
1010
import { WelcomeStateProvider } from './stateProvider';
1111
import '../shared/components/gitlens-logo';
1212
import { welcomeStyles } from './welcome.css';
13+
import './components/feature-carousel';
1314

1415
@customElement('gl-welcome-app')
1516
export class GlWelcomeApp extends GlAppHost<State> {
@@ -23,19 +24,43 @@ export class GlWelcomeApp extends GlAppHost<State> {
2324
return new WelcomeStateProvider(this, bootstrap, ipc, logger);
2425
}
2526

27+
@property({ type: String })
28+
webroot?: string;
29+
2630
override render(): unknown {
2731
return html`
2832
<div class="welcome scrollable">
29-
<div class="welcome__section welcome__header">
33+
<div class="section header">
3034
<gitlens-logo></gitlens-logo>
3135
<h1>GitLens is now installed in Cursor</h1>
3236
<p>
3337
Understand every line of code — instantly. GitLens reveals authorship, activity, and history
3438
inside the editor
3539
</p>
3640
</div>
37-
<div class="welcome__section">
38-
<p>With <span class="welcome__accent">PRO</span> subscription you get more</p>
41+
<div class="section">
42+
<p>With <span class="accent">PRO</span> subscription you get more</p>
43+
</div>
44+
45+
<div class="section">
46+
<gl-feature-carousel>
47+
<gl-feature-card>
48+
<img slot="image" src="${this.webroot ?? ''}/media/feature-graph.webp" alt="Commit Graph" />
49+
<h1>Commit Graph</h1>
50+
<p>Visualize your repository's history and interact with commits</p>
51+
<p><a href="command:gitlens.showGraph">Open Commit Graph</a></p>
52+
</gl-feature-card>
53+
<gl-feature-card>
54+
<img
55+
slot="image"
56+
src="${this.webroot ?? ''}/media/feature-timeline.webp"
57+
alt="Visual File History"
58+
/>
59+
<h1>Visual File History</h1>
60+
<p>Track changes to any file over time</p>
61+
<p><a href="command:gitlens.showTimelineView">Open Visual File History</a></p>
62+
</gl-feature-card>
63+
</gl-feature-carousel>
3964
</div>
4065
4166
<div>

0 commit comments

Comments
 (0)