Browse Source

Adds new Account view & brand new Home experience

main
Keith Daulton 1 year ago
committed by Eric Amodio
parent
commit
dffefeb411
26 changed files with 1173 additions and 2322 deletions
  1. +33
    -15
      package.json
  2. +2
    -0
      src/commands/showView.ts
  3. +2
    -1
      src/constants.ts
  4. +7
    -0
      src/container.ts
  5. +7
    -7
      src/plus/subscription/subscriptionService.ts
  6. +120
    -0
      src/plus/webviews/account/accountWebview.ts
  7. +18
    -0
      src/plus/webviews/account/protocol.ts
  8. +22
    -0
      src/plus/webviews/account/registration.ts
  9. +2
    -1
      src/telemetry/usageTracker.ts
  10. +0
    -109
      src/webviews/apps/home/components/card-section.ts
  11. +0
    -266
      src/webviews/apps/home/components/plus-banner.ts
  12. +0
    -134
      src/webviews/apps/home/components/stepped-section.ts
  13. +106
    -281
      src/webviews/apps/home/home.html
  14. +82
    -339
      src/webviews/apps/home/home.scss
  15. +5
    -289
      src/webviews/apps/home/home.ts
  16. +21
    -0
      src/webviews/apps/plus/account/account.html
  17. +121
    -0
      src/webviews/apps/plus/account/account.scss
  18. +105
    -0
      src/webviews/apps/plus/account/account.ts
  19. +19
    -53
      src/webviews/apps/plus/account/components/header-card.ts
  20. +4
    -4
      src/webviews/apps/plus/account/components/plus-content.ts
  21. +3
    -3
      src/webviews/apps/shared/components/account/account-badge.ts
  22. +7
    -282
      src/webviews/home/homeWebview.ts
  23. +1
    -54
      src/webviews/home/protocol.ts
  24. +2
    -0
      webpack.config.js

+ 33
- 15
package.json View File

@ -4606,6 +4606,11 @@
"category": "GitLens"
},
{
"command": "gitlens.showAccountView",
"title": "Show Account View",
"category": "GitLens"
},
{
"command": "gitlens.showInCommitGraph",
"title": "Open in Commit Graph",
"category": "GitLens+",
@ -6381,6 +6386,12 @@
"icon": "$(refresh)"
},
{
"command": "gitlens.views.account.refresh",
"title": "Refresh",
"category": "GitLens",
"icon": "$(refresh)"
},
{
"command": "gitlens.views.lineHistory.changeBase",
"title": "Change Base...",
"category": "GitLens",
@ -6832,12 +6843,6 @@
"category": "GitLens"
},
{
"command": "gitlens.views.timeline.openInTab",
"title": "Open in Editor Area",
"category": "GitLens",
"icon": "$(link-external)"
},
{
"command": "gitlens.views.timeline.refresh",
"title": "Refresh",
"category": "GitLens",
@ -7721,6 +7726,10 @@
"when": "gitlens:enabled"
},
{
"command": "gitlens.showAccountView",
"when": "gitlens:enabled"
},
{
"command": "gitlens.showInCommitGraph",
"when": "false"
},
@ -8905,6 +8914,10 @@
"when": "false"
},
{
"command": "gitlens.views.account.refresh",
"when": "false"
},
{
"command": "gitlens.views.lineHistory.changeBase",
"when": "false"
},
@ -9233,10 +9246,6 @@
"when": "false"
},
{
"command": "gitlens.views.timeline.openInTab",
"when": "false"
},
{
"command": "gitlens.views.timeline.refresh",
"when": "false"
},
@ -10343,6 +10352,11 @@
"group": "navigation@99"
},
{
"command": "gitlens.views.account.refresh",
"when": "view =~ /^gitlens\\.views\\.account/",
"group": "navigation@99"
},
{
"command": "gitlens.showLineHistoryView",
"when": "!gitlens:hasVirtualFolders && view =~ /^gitlens\\.views\\.fileHistory/",
"group": "8_gitlens_toggles@0"
@ -10618,11 +10632,6 @@
"group": "5_gitlens@0"
},
{
"command": "gitlens.views.timeline.openInTab",
"when": "view =~ /^gitlens\\.views\\.timeline/",
"group": "navigation@98"
},
{
"command": "gitlens.views.timeline.refresh",
"when": "view =~ /^gitlens\\.views\\.timeline/",
"group": "navigation@99"
@ -13701,6 +13710,15 @@
"icon": "$(gitlens-contributors-view)",
"initialSize": 1,
"visibility": "collapsed"
},
{
"type": "webview",
"id": "gitlens.views.account",
"name": "GitKraken Account",
"contextualTitle": "GitLens",
"icon": "$(gitlens-gitlens)",
"initialSize": 1,
"visibility": "collapsed"
}
],
"gitlensInspect": [

+ 2
- 0
src/commands/showView.ts View File

@ -44,6 +44,8 @@ export class ShowViewCommand extends Command {
return this.container.fileHistoryView.show();
case Commands.ShowHomeView:
return this.container.homeView.show();
case Commands.ShowAccountView:
return this.container.accountView.show();
case Commands.ShowGraphView:
return this.container.graphView.show();
case Commands.ShowLineHistoryView:

+ 2
- 1
src/constants.ts View File

@ -239,6 +239,7 @@ export const enum Commands {
ShowGraphPage = 'gitlens.showGraphPage',
ShowGraphView = 'gitlens.showGraphView',
ShowHomeView = 'gitlens.showHomeView',
ShowAccountView = 'gitlens.showAccountView',
ShowInCommitGraph = 'gitlens.showInCommitGraph',
ShowInDetailsView = 'gitlens.showInDetailsView',
ShowLastQuickPick = 'gitlens.showLastQuickPick',
@ -311,7 +312,7 @@ export const enum Commands {
export type CustomEditorIds = 'rebase';
export type WebviewIds = 'graph' | 'settings' | 'timeline' | 'welcome' | 'focus';
export type WebviewViewIds = 'commitDetails' | 'graph' | 'graphDetails' | 'home' | 'timeline';
export type WebviewViewIds = 'commitDetails' | 'graph' | 'graphDetails' | 'home' | 'timeline' | 'account';
export type ContextKeys =
| `${typeof extensionPrefix}:action:${string}`

+ 7
- 0
src/container.ts View File

@ -23,6 +23,7 @@ import { IntegrationAuthenticationService } from './plus/integrationAuthenticati
import { SubscriptionAuthenticationProvider } from './plus/subscription/authenticationProvider';
import { ServerConnection } from './plus/subscription/serverConnection';
import { SubscriptionService } from './plus/subscription/subscriptionService';
import { registerAccountWebviewView } from './plus/webviews/account/registration';
import { registerFocusWebviewPanel } from './plus/webviews/focus/registration';
import {
registerGraphWebviewCommands,
@ -250,6 +251,7 @@ export class Container {
this._disposables.push((this._searchAndCompareView = new SearchAndCompareView(this)));
this._disposables.push((this._homeView = registerHomeWebviewView(this._webviews)));
this._disposables.push((this._accountView = registerAccountWebviewView(this._webviews)));
if (configuration.get('terminalLinks.enabled')) {
this._disposables.push((this._terminalLinks = new GitTerminalLinkProvider(this)));
@ -484,6 +486,11 @@ export class Container {
return this._homeView;
}
private readonly _accountView: WebviewViewProxy;
get accountView() {
return this._accountView;
}
@memoize()
get id() {
return this._context.extension.id;

+ 7
- 7
src/plus/subscription/subscriptionService.ts View File

@ -239,7 +239,7 @@ export class SubscriptionService implements Disposable {
// Abort any waiting authentication to ensure we can start a new flow
await this.container.subscriptionAuthentication.abort();
void this.showHomeView();
void this.showAccountView();
const session = await this.ensureSession(true);
const loggedIn = Boolean(session);
@ -366,7 +366,7 @@ export class SubscriptionService implements Disposable {
Uri.joinPath(this.baseAccountUri, 'subscription').with({ query: 'product=gitlens&license=PRO' }),
);
}
await this.showHomeView();
await this.showAccountView();
}
@gate()
@ -376,7 +376,7 @@ export class SubscriptionService implements Disposable {
const scope = getLogScope();
void this.showHomeView(true);
void this.showAccountView(true);
const session = await this.ensureSession(false);
if (session == null) return false;
@ -429,11 +429,11 @@ export class SubscriptionService implements Disposable {
}
@log()
async showHomeView(silent: boolean = false): Promise<void> {
async showAccountView(silent: boolean = false): Promise<void> {
if (silent && !configuration.get('plusFeatures.enabled', undefined, true)) return;
if (!this.container.homeView.visible) {
await executeCommand(Commands.ShowHomeView);
if (!this.container.accountView.visible) {
await executeCommand(Commands.ShowAccountView);
}
}
@ -448,7 +448,7 @@ export class SubscriptionService implements Disposable {
let { plan, previewTrial } = this._subscription;
if (previewTrial != null) {
void this.showHomeView();
void this.showAccountView();
if (!silent && plan.effective.id === SubscriptionPlanId.Free) {
const confirm: MessageItem = { title: 'Extend Your Trial', isCloseAffordance: true };

+ 120
- 0
src/plus/webviews/account/accountWebview.ts View File

@ -0,0 +1,120 @@
import { Disposable, window } from 'vscode';
import { getAvatarUriFromGravatarEmail } from '../../../avatars';
import type { Container } from '../../../container';
import type { RepositoriesVisibility } from '../../../git/gitProviderService';
import type { Subscription } from '../../../subscription';
import { registerCommand } from '../../../system/command';
import type { Deferrable } from '../../../system/function';
import { debounce } from '../../../system/function';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type { State } from './protocol';
import { DidChangeSubscriptionNotificationType } from './protocol';
export class AccountWebviewProvider implements WebviewProvider<State> {
private readonly _disposable: Disposable;
constructor(private readonly container: Container, private readonly host: WebviewController<State>) {
this._disposable = Disposable.from(this.container.subscription.onDidChange(this.onSubscriptionChanged, this));
}
dispose() {
this._disposable.dispose();
}
private onSubscriptionChanged(e: SubscriptionChangeEvent) {
void this.notifyDidChangeData(e.current);
}
onVisibilityChanged(visible: boolean): void {
if (!visible) {
this._validateSubscriptionDebounced?.cancel();
return;
}
queueMicrotask(() => void this.validateSubscription());
}
onWindowFocusChanged(focused: boolean): void {
if (!focused || !this.host.visible) {
this._validateSubscriptionDebounced?.cancel();
return;
}
queueMicrotask(() => void this.validateSubscription());
}
registerCommands(): Disposable[] {
return [registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this)];
}
includeBootstrap(): Promise<State> {
return this.getState();
}
private async getRepoVisibility(): Promise<RepositoriesVisibility> {
const visibility = await this.container.git.visibility();
return visibility;
}
private async getSubscription(subscription?: Subscription) {
const sub = subscription ?? (await this.container.subscription.getSubscription(true));
let avatar;
if (sub.account?.email) {
avatar = getAvatarUriFromGravatarEmail(sub.account.email, 34).toString();
} else {
avatar = `${this.host.getWebRoot() ?? ''}/media/gitlens-logo.webp`;
}
return {
subscription: sub,
avatar: avatar,
};
}
private async getState(subscription?: Subscription): Promise<State> {
const subscriptionResult = await this.getSubscription(subscription);
return {
timestamp: Date.now(),
webroot: this.host.getWebRoot(),
subscription: subscriptionResult.subscription,
avatar: subscriptionResult.avatar,
};
}
private notifyDidChangeData(subscription?: Subscription) {
if (!this.host.ready) return false;
return window.withProgress({ location: { viewId: this.host.id } }, async () => {
const sub = await this.getSubscription(subscription);
return this.host.notify(DidChangeSubscriptionNotificationType, {
...sub,
});
});
}
private _validateSubscriptionDebounced: Deferrable<AccountWebviewProvider['validateSubscription']> | undefined =
undefined;
private async validateSubscription(): Promise<void> {
if (this._validateSubscriptionDebounced == null) {
this._validateSubscriptionDebounced = debounce(this.validateSubscriptionCore, 1000);
}
await this._validateSubscriptionDebounced();
}
private _validating: Promise<void> | undefined;
private async validateSubscriptionCore() {
if (this._validating == null) {
this._validating = this.container.subscription.validate();
try {
await this._validating;
} finally {
this._validating = undefined;
}
}
}
}

+ 18
- 0
src/plus/webviews/account/protocol.ts View File

@ -0,0 +1,18 @@
import type { Subscription } from '../../../subscription';
import { IpcNotificationType } from '../../../webviews/protocol';
export interface State {
timestamp: number;
webroot?: string;
subscription: Subscription;
avatar?: string;
}
export interface DidChangeSubscriptionParams {
subscription: Subscription;
avatar?: string;
}
export const DidChangeSubscriptionNotificationType = new IpcNotificationType<DidChangeSubscriptionParams>(
'subscription/didChange',
);

+ 22
- 0
src/plus/webviews/account/registration.ts View File

@ -0,0 +1,22 @@
import type { WebviewsController } from '../../../webviews/webviewsController';
import type { State } from './protocol';
export function registerAccountWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State>(
{
id: 'gitlens.views.account',
fileName: 'account.html',
title: 'GitKraken Account',
contextKeyPrefix: `gitlens:webviewView:account`,
trackingFeature: 'accountView',
plusFeature: false,
webviewHostOptions: {
retainContextWhenHidden: false,
},
},
async (container, host) => {
const { AccountWebviewProvider } = await import(/* webpackChunkName: "account" */ './accountWebview');
return new AccountWebviewProvider(container, host);
},
);
}

+ 2
- 1
src/telemetry/usageTracker.ts View File

@ -31,7 +31,8 @@ export type TrackedUsageFeatures =
| 'timelineView'
| 'welcomeWebview'
| 'workspaceView'
| 'focusWebview';
| 'focusWebview'
| 'accountView';
export type TrackedUsageKeys = `${TrackedUsageFeatures}:shown`;
export type UsageChangeEvent = {

+ 0
- 109
src/webviews/apps/home/components/card-section.ts View File

@ -1,109 +0,0 @@
import { attr, css, customElement, FASTElement, html, when } from '@microsoft/fast-element';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/code-icon';
const template = html<CardSection>`<template role="region">
${when(
x => x.noHeading === false,
html<CardSection>`<header>
<div class="heading" role="heading" aria-level="${x => x.headingLevel}">
<slot name="heading"></slot>
<small class="description"><slot name="description"></slot></small>
</div>
${when(
x => x.dismissable,
html<CardSection>`<button
class="dismiss"
type="button"
@click="${(x, c) => x.handleDismiss(c.event)}"
title="dismiss"
aria-label="dismiss"
>
<code-icon icon="close"></code-icon>
</button>`,
)}
</header>`,
)}
<div class="content"><slot></slot></div>
</template>`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
display: block;
padding: 1.2rem;
background-color: var(--card-background);
margin-bottom: 1rem;
border-radius: 0.4rem;
background-repeat: no-repeat;
background-size: cover;
transition: aspect-ratio linear 100ms, background-color linear 100ms;
}
:host(:hover) {
background-color: var(--card-hover-background);
}
header {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 0.4rem;
margin-bottom: 1rem;
}
.dismiss {
width: 2rem;
height: 2rem;
padding: 0;
font-size: var(--vscode-editor-font-size);
line-height: 2rem;
font-family: inherit;
border: none;
color: inherit;
background: none;
text-align: left;
cursor: pointer;
opacity: 0.5;
flex: none;
text-align: center;
}
.dismiss:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 0.2rem;
}
.heading {
text-transform: uppercase;
}
.description {
margin-left: 0.2rem;
text-transform: none;
/* color needs to come from some sort property */
color: #b68cd8;
}
`;
@customElement({ name: 'card-section', template: template, styles: styles })
export class CardSection extends FASTElement {
@attr({ attribute: 'no-heading', mode: 'boolean' })
noHeading = false;
@attr({ attribute: 'heading-level', converter: numberConverter })
headingLevel = 2;
@attr({ mode: 'boolean' })
dismissable = false;
@attr({ mode: 'boolean' })
expanded = true;
handleDismiss(_e: Event) {
this.$emit('dismiss');
}
}

+ 0
- 266
src/webviews/apps/home/components/plus-banner.ts View File

@ -1,266 +0,0 @@
import { attr, css, customElement, FASTElement, html, observable, volatile, when } from '@microsoft/fast-element';
import { SubscriptionState } from '../../../../subscription';
import { pluralize } from '../../../../system/string';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/code-icon';
const template = html<PlusBanner>`
${when(
x => x.state === SubscriptionState.Free,
html<PlusBanner>`
<h3>
<a title="Learn more about GitLens+ features" href="command:gitlens.plus.learn"
>Powerful, additional features</a
>
that enhance your GitLens experience.
</h3>
${when(
y => y.hasRepositories,
html<PlusBanner>`
<p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.startPreviewTrial')}"
>Try GitLens+ features on private repos</vscode-button
>
</p>
`,
)}
`,
)}
${when(
x => x.state === SubscriptionState.Paid,
html<PlusBanner>`
<h3>Welcome to ${x => x.planName}!</h3>
<p class="mb-0">
You have access to
<a title="Learn more about GitLens+ features" href="command:gitlens.plus.learn">GitLens+ features</a>
on any repo.
</p>
`,
)}
${when(
x => x.state === SubscriptionState.FreeInPreviewTrial,
html<PlusBanner>`
<h3>GitLens Pro Trial</h3>
<p>
You have ${x => x.daysRemaining} left in your 3-day GitLens Pro trial. Don't worry if you need more
time, you can extend your trial for an additional free 7-days of
<a title="Learn more about GitLens+ features" href="command:gitlens.plus.learn">GitLens+ features</a> on
private repos.
</p>
${when(
y => y.hasRepositories,
html<PlusBanner>`
<p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.purchase')}"
>Upgrade to Pro</vscode-button
>
</p>
`,
)}
`,
)}
${when(
x => x.state === SubscriptionState.FreePlusInTrial,
html<PlusBanner>`
<h3>GitLens Pro Trial</h3>
<p class="mb-1">
You have ${x => x.daysRemaining} left in your GitLens Pro trial. Once your trial ends, you'll continue
to have access to
<a title="Learn more about GitLens+ features" href="command:gitlens.plus.learn">GitLens+ features</a> on
local and public repos, while upgrading to GitLens Pro gives you access on private repos.
</p>
`,
)}
${when(
x => x.state === SubscriptionState.FreePreviewTrialExpired,
html<PlusBanner>`
<h3>Extend Your GitLens Pro Trial</h3>
<p>
Your free 3-day GitLens Pro trial has ended, extend your trial to get an additional free 7-days of
GitLens+ features on private repos.
</p>
${when(
y => y.hasRepositories,
html<PlusBanner>`
<p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.loginOrSignUp')}"
>Extend Pro Trial</vscode-button
>
</p>
`,
)}
`,
)}
${when(
x => x.state === SubscriptionState.FreePlusTrialExpired,
html<PlusBanner>`
<h3>GitLens Pro Trial Expired</h3>
<p>
Your GitLens Pro trial has ended, please upgrade to GitLens Pro to continue to use GitLens+ features on
private repos.
</p>
${when(
y => y.hasRepositories,
html<PlusBanner>`
<p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.purchase')}"
>Upgrade to Pro</vscode-button
>
</p>
`,
)}
`,
)}
${when(
x => x.state === SubscriptionState.VerificationRequired,
html<PlusBanner>`
<h3>Please verify your email</h3>
<p class="alert__message">
Before you can also use GitLens+ features on private repos, please verify your email address.
</p>
<p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.resendVerification')}"
>Resend Verification Email</vscode-button
>
</p>
<p class="mb-1">
<vscode-button @click="${x => x.fireAction('command:gitlens.plus.validate')}"
>Refresh Verification Status</vscode-button
>
</p>
`,
)}
${when(
x =>
[
SubscriptionState.Free,
SubscriptionState.FreePreviewTrialExpired,
SubscriptionState.FreePlusTrialExpired,
].includes(x.state) && x.hasRepositories,
html<PlusBanner>`
<p class="mb-0">
${when(
x => x.plus,
html<PlusBanner>`<a class="minimal" href="command:gitlens.plus.hide">Hide GitLens+ features</a>`,
)}
${when(
x => !x.plus,
html<PlusBanner>`<a href="command:gitlens.plus.restore">Restore GitLens+ features</a>`,
)}
</p>
`,
)}
${when(
x => !x.hasRepositories,
html<PlusBanner>`
<p class="mb-0">
To use GitLens+, open a folder containing a git repository or clone from a URL from the Explorer.
</p>
`,
)}
`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
display: block;
text-align: center;
}
a {
color: var(--vscode-textLink-foreground);
text-decoration: none;
}
a:focus {
outline-color: var(--focus-border);
}
a:hover {
text-decoration: underline;
}
h3,
p {
margin-top: 0;
}
h3 a {
color: inherit;
text-decoration: underline;
text-decoration-color: var(--color-foreground--50);
}
h3 a:hover {
text-decoration-color: inherit;
}
.mb-1 {
margin-bottom: 0.4rem;
}
.mb-0 {
margin-bottom: 0;
}
.minimal {
color: var(--color-foreground--50);
font-size: 1rem;
position: relative;
top: -0.2rem;
}
`;
@customElement({ name: 'plus-banner', template: template, styles: styles })
export class PlusBanner extends FASTElement {
@attr({ converter: numberConverter })
days = 0;
@attr({ converter: numberConverter })
state: SubscriptionState = SubscriptionState.Free;
@attr
plan = '';
@attr
visibility: 'local' | 'public' | 'mixed' | 'private' = 'public';
@attr({ mode: 'boolean' })
plus = true;
@observable
hasRepositories = false;
get daysRemaining() {
if (this.days < 1) {
return 'less than one day';
}
return pluralize('day', this.days);
}
get isFree() {
return ['local', 'public'].includes(this.visibility);
}
@volatile
get planName() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
return 'GitLens Free';
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return 'GitLens Pro (Trial)';
case SubscriptionState.VerificationRequired:
return `${this.plan} (Unverified)`;
default:
return this.plan;
}
}
fireAction(command: string) {
this.$emit('action', command);
}
}

+ 0
- 134
src/webviews/apps/home/components/stepped-section.ts View File

@ -1,134 +0,0 @@
import { attr, css, customElement, FASTElement, html } from '@microsoft/fast-element';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/code-icon';
const template = html<SteppedSection>`<template role="region">
<header class="heading" role="heading" aria-level="${x => x.headingLevel}">
<button
id="button"
class="button"
type="button"
aria-expanded="${x => !x.completed}"
aria-controls="content"
@click="${(x, c) => x.handleClick(c.event)}"
>
<slot name="heading"></slot>
<span class="description"><slot name="description"></slot></span>
</button>
</header>
<div class="content${x => (x.completed ? ' is-hidden' : '')}" id="content" aria-labelledby="button">
<slot></slot>
</div>
<span class="checkline">
<span
class="checkbox"
title="${x => (x.completed ? 'Uncheck step' : 'Check step')}"
@click="${(x, c) => x.handleClick(c.event)}"
><code-icon
class="check-icon"
icon="${x => (x.completed ? 'pass-filled' : 'circle-large-outline')}"
></code-icon
><code-icon class="check-hover-icon" icon="${x => (x.completed ? 'pass-filled' : 'pass')}"></code-icon
></span>
</span>
</template>`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
display: grid;
gap: 0 0.8rem;
grid-template-columns: 16px auto;
grid-auto-flow: column;
margin-bottom: 2.4rem;
}
.button {
width: 100%;
padding: 0.1rem 0 0 0;
font-size: var(--vscode-editor-font-size);
line-height: 1.6rem;
font-family: inherit;
border: none;
color: inherit;
background: none;
text-align: left;
text-transform: uppercase;
cursor: pointer;
}
.button:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 0.2rem;
}
.checkline {
position: relative;
grid-column: 1;
grid-row: 1 / span 2;
color: var(--vscode-textLink-foreground);
}
:host(:not(:last-of-type)) .checkline:after {
content: '';
position: absolute;
border-left: 0.1rem solid currentColor;
width: 0;
top: 1.6rem;
bottom: -2.4rem;
left: 50%;
transform: translateX(-50%);
opacity: 0.3;
}
.checkbox {
cursor: pointer;
}
.checkbox code-icon {
pointer-events: none;
}
.heading:hover ~ .checkline .check-icon,
.checkbox:hover .check-icon {
display: none;
}
.check-hover-icon {
display: none;
}
.heading:hover ~ .checkline .check-hover-icon,
.checkbox:hover .check-hover-icon {
display: unset;
}
.content {
margin-top: 1rem;
}
.content.is-hidden {
display: none;
}
.description {
margin-left: 0.6rem;
text-transform: none;
opacity: 0.5;
}
`;
@customElement({ name: 'stepped-section', template: template, styles: styles })
export class SteppedSection extends FASTElement {
@attr({ attribute: 'heading-level', converter: numberConverter })
headingLevel = 2;
@attr({ mode: 'boolean' })
completed = false;
handleClick(_e: Event) {
this.completed = !this.completed;
this.$emit('complete', this.completed);
}
}

+ 106
- 281
src/webviews/apps/home/home.html View File

@ -15,21 +15,21 @@
href="https://help.gitkraken.com/gitlens/gitlens-release-notes-current/"
aria-label="What's New"
title="What's New"
><span class="codicon codicon-rocket"></span><span>What's New</span></a
><code-icon icon="rocket"></code-icon><span>What's New</span></a
>
<a
class="inline-nav__link inline-nav__link--text"
href="https://help.gitkraken.com/gitlens/gitlens-home/"
aria-label="Help Center"
title="Help Center"
><span class="codicon codicon-question"></span><span>Help</span></a
><code-icon icon="question"></code-icon><span>Help</span></a
>
<a
class="inline-nav__link inline-nav__link--text"
href="https://github.com/gitkraken/vscode-gitlens/issues"
aria-label="Feedback"
title="Feedback"
><span class="codicon codicon-feedback"></span><span>Feedback</span></a
><code-icon icon="feedback"></code-icon><span>Feedback</span></a
>
</div>
<div class="inline-nav__group">
@ -38,33 +38,33 @@
href="https://github.com/gitkraken/vscode-gitlens/discussions"
aria-label="GitHub Discussions"
title="GitHub Discussions"
><span class="codicon codicon-comment-discussion"></span
><code-icon icon="comment-discussion"></code-icon
></a>
<a
class="inline-nav__link"
href="https://github.com/gitkraken/vscode-gitlens"
aria-label="GitHub Repo"
title="GitHub Repo"
><span class="codicon codicon-github"></span
><code-icon icon="github"></code-icon
></a>
<a
class="inline-nav__link"
href="https://twitter.com/gitlens"
aria-label="@gitlens on Twitter"
title="@gitlens on Twitter"
><span class="codicon codicon-twitter"></span
><code-icon icon="twitter"></code-icon
></a>
<a
class="inline-nav__link"
href="https://gitkraken.com/gitlens?utm_source=gitlens-extension&utm_medium=in-app-links&utm_campaign=gitlens-logo-links"
aria-label="GitLens Website"
title="GitLens Website"
><span class="codicon codicon-globe"></span
><code-icon icon="globe"></code-icon
></a>
</div>
</nav>
</div>
<header class="home__header">
<header class="home__header" id="header" hidden>
<div id="no-repo-alert" class="alert alert--info mb-0" aria-hidden="true" hidden>
<h1 class="alert__title">No repository detected</h1>
<div class="alert__description">
@ -72,9 +72,7 @@
To use GitLens, open a folder containing a git repository or clone from a URL from the Explorer.
</p>
<p class="centered">
<vscode-button data-action="command:workbench.view.explorer"
>Open a Folder or Repository</vscode-button
>
<gk-button data-action="command:workbench.view.explorer">Open a Folder or Repository</gk-button>
</p>
<p class="mb-0">
If you have opened a folder with a repository, please let us know by
@ -92,7 +90,7 @@
not being owned by the current user.
</p>
<p class="centered">
<vscode-button data-action="command:workbench.view.scm">Manage in Source Control</vscode-button>
<gk-button data-action="command:workbench.view.scm">Manage in Source Control</gk-button>
</p>
</div>
</div>
@ -101,282 +99,109 @@
<div class="alert__description">
<p>Unable to open repositories in Restricted Mode.</p>
<p class="centered">
<vscode-button data-action="command:workbench.trust.manage"
>Manage Workspace Trust</vscode-button
>
<gk-button data-action="command:workbench.trust.manage">Manage Workspace Trust</gk-button>
</p>
</div>
</div>
<header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card>
</header>
<main class="home__main scrollable" id="main" tabindex="-1">
<style nonce="#{cspNonce}">
.video-banner {
--video-banner-bg: url(#{webroot}/media/getting-started.webp);
}
.vscode-high-contrast .video-banner,
.vscode-dark .video-banner {
--video-banner-play: url(#{webroot}/media/play-button.webp);
}
.vscode-high-contrast-light .video-banner,
.vscode-light .video-banner {
--video-banner-play: url(#{webroot}/media/play-button-dark.webp);
}
.gl-plus-banner {
background-image: url(#{webroot}/media/gitlens-backdrop-opacity.webp);
}
</style>
<div class="stepped-sections">
<stepped-section id="welcome">
<span slot="heading">Welcome to GitLens 13</span>
<p>
GitLens supercharges Git inside VS Code and unlocks the untapped knowledge within each
repository.
</p>
<a class="video-banner" href="https://www.youtube.com/watch?v=UQPb73Zz9qk">
<span>Get Started</span> <small>Tutorial Video</small>
</a>
<ul class="icon-list mb-0">
<li>
<code-icon icon="circle-filled"></code-icon>
<a href="command:gitlens.showWelcomePage?%22quick-setup%22">Quick Setup</a> &mdash; quickly
personalize GitLens to your needs.
</li>
<li>
<code-icon icon="circle-filled"></code-icon>
<a href="command:gitlens.showSettingsPage">Interactive Settings editor</a> &mdash; fine-tune
your GitLens experience.
</li>
</ul>
</stepped-section>
<stepped-section id="features">
<span slot="heading">Features</span>
<span slot="description">always free and accessible</span>
<p>
GitLens is deeply integrated into many areas and aspects of VS Code, especially editors and
views. Learn more in the <a href="command:gitlens.getStarted">Feature Walkthrough</a>.
</p>
<card-section no-heading id="no-repo" aria-hidden="true">
<p class="centered mb-0">
To use GitLens, open a folder containing a git repository or clone from a URL from the
Explorer.
</p>
</card-section>
<div class="activitybar-banner">
<div class="activitybar-banner__content">
<p>
Find many features by opening the
<a href="command:workbench.view.scm">Source Control Side Bar</a>.
</p>
<p>
Click on
<span aria-hidden="true"
>the icon <span class="not-small">to the left</span
><span class="only-small">below</span></span
><span class="sr-only">a layout option</span> to set the location of your GitLens views.
</p>
</div>
<nav class="activitybar-banner__media" aria-label="GitLens Layout">
<a
href="command:gitlens.setViewsLayout?%7B%22layout%22%3A%22scm%22%7D"
class="activitybar-banner__nav-item"
title="Move views to the Source Control side bar"
aria-label="Move views to the Source Control side bar"
></a>
<a
href="command:gitlens.setViewsLayout?%7B%22layout%22%3A%22gitlens%22%7D"
class="activitybar-banner__nav-item"
title="Move views to the GitLens side bar"
aria-label="Move views to the GitLens side bar"
></a>
<svg
class="svg"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 68 91"
aria-hidden="true"
style="max-width: 9.2rem"
>
<rect x=".5" y=".5" width="40" height="90" rx="16.5" stroke="url(#a)" />
<rect x="3" y="3" width="35" height="85" rx="14" fill="#333" class="svg__bar" />
<g clip-path="url(#b)" class="svg__icon" data-gitlens-layout="gitlens">
<path
d="m22.08 63-2.14-2.14.75-.75 2.14 2.13-.75.76ZM18.73 66.08v-5.16h1.06v5.16h-1.06Z"
/>
<path
d="M19.26 69.17a1.6 1.6 0 1 0 0-3.2 1.6 1.6 0 0 0 0 3.2ZM23.51 65.27a1.6 1.6 0 1 0 0-3.19 1.6 1.6 0 0 0 0 3.2ZM19.26 61.02a1.6 1.6 0 1 0 0-3.19 1.6 1.6 0 0 0 0 3.2Z"
/>
<path
d="M20.5 55.89a7.61 7.61 0 1 0 0 15.22 7.61 7.61 0 0 0 0-15.22ZM12 63.5a8.5 8.5 0 1 1 17 0 8.5 8.5 0 0 1-17 0Z"
/>
</g>
<path
fill="#fff"
d="M3 52h1v23H3z"
class="svg__indicator"
data-gitlens-layout="gitlens"
/>
<g clip-path="url(#c)" class="svg__icon" data-gitlens-layout="scm">
<path
d="M26.76 25.17a2.8 2.8 0 1 0-3.6 2.67 2.24 2.24 0 0 1-2.01 1.25h-2.24c-.83 0-1.63.31-2.24.87v-5.41a2.8 2.8 0 1 0-1.13 0v6.84a2.83 2.83 0 1 0 1.37.07 2.24 2.24 0 0 1 2-1.25h2.24a3.36 3.36 0 0 0 3.17-2.28 2.8 2.8 0 0 0 2.44-2.76ZM14.42 21.8a1.68 1.68 0 1 1 3.37 0 1.68 1.68 0 0 1-3.37 0Zm3.37 12.33a1.68 1.68 0 1 1-3.37 0 1.68 1.68 0 0 1 3.37 0Zm6.16-7.28a1.68 1.68 0 1 1 0-3.37 1.68 1.68 0 0 1 0 3.37Z"
/>
</g>
<path fill="#fff" d="M3 16h1v24H3z" class="svg__indicator" data-gitlens-layout="scm" />
<path
d="M67.67 64a2.67 2.67 0 1 0-5.34 0 2.67 2.67 0 0 0 5.34 0ZM49 64l5 2.89V61.1L49 64Zm16-.5H53.5v1H65v-1Z"
fill="red"
class="svg__arrow"
data-gitlens-layout="gitlens"
/>
<path
d="M67.67 28a2.67 2.67 0 1 0-5.34 0 2.67 2.67 0 0 0 5.34 0ZM49 28l5 2.89V25.1L49 28Zm16-.5H53.5v1H65v-1Z"
fill="#007FD5"
class="svg__arrow"
data-gitlens-layout="scm"
/>
<defs>
<clipPath id="b">
<path fill="#fff" transform="translate(12 55)" d="M0 0h17v17H0z" />
</clipPath>
<clipPath id="c">
<path fill="#fff" transform="translate(11 19)" d="M0 0h18v18H0z" />
</clipPath>
<linearGradient
id="a"
x1="20.5"
y1="-23.19"
x2="20.5"
y2="91"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#333" class="svg__outline" />
<stop offset="1" stop-color="#333" stop-opacity="0" class="svg__outline" />
<stop offset="1" stop-color="#333" stop-opacity="0" class="svg__outline" />
</linearGradient>
</defs>
</svg>
</nav>
</div>
</stepped-section>
<stepped-section id="plus">
<span slot="heading">GitLens+ Features</span>
<span slot="description">want even more from GitLens?</span>
<card-section class="gl-plus-banner mb-1" no-heading>
<div class="centered plus-banner-text">
<plus-banner id="plus-banner"></plus-banner>
</div>
</card-section>
<plus-content id="plus-content"></plus-content>
</stepped-section>
<stepped-section id="integrations">
<span slot="heading">Integrations</span>
<p>GitLens provides issue and pull request auto-linking with many Git hosting services.</p>
<p class="mb-0">
Rich integrations with GitHub & GitLab provide more detailed hover information for auto-linked
issues and pull requests, pull requests associated with branches and commits, and avatars.
</p>
</stepped-section>
</div>
<!-- check for gitlens+ -->
<div id="plus-sections">
<card-section dismissable id="focus-view-preview">
<span slot="heading">Focus View ✨ (preview)</span>
<a title="Open the Focus view" href="command:gitlens.showFocusPage"
><img
src="#{webroot}/media/plus-focus-view-preview.webp"
alt="Focus view Screenshot"
class="plus-section-thumb mb-1"
/></a>
<p class="mb-0">
The
<a title="Open the Focus view" href="command:gitlens.showFocusPage">Focus View</a>
provides you with a comprehensive list of all your most important work across your connected
GitHub repos.
</p>
</card-section>
<card-section dismissable id="commit-graph">
<span slot="heading">Commit Graph ✨</span>
<a
title="Learn more about the Commit Graph"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.commitGraph%22"
><img
src="#{webroot}/media/plus-commit-graph-illustrated.webp"
alt="Commit Graph illustration"
class="plus-section-thumb mb-1"
/></a>
<p>
The
<a
title="Learn more about the Commit Graph"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.commitGraph%22"
>Commit Graph</a
>
helps you easily visualize and keep track of all work in progress.
</p>
<p class="mb-0">
Use the rich commit search to find exactly what you're looking for. It's powerful filters allow
you to search by a specific commit, message, author, a changed file or files, or even a specific
code change.
</p>
</card-section>
<card-section dismissable id="visual-file-history">
<span slot="heading">Visual File History ✨</span>
<a
title="Learn more about the Visual File History"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.visualFileHistory%22"
><img
src="#{webroot}/media/plus-visual-file-history-illustrated.webp"
alt="Visual File History illustration"
class="plus-section-thumb mb-1"
/></a>
<p>
The
<a
title="Learn more about the Visual File History"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.visualFileHistory%22"
>Visual File History</a
>
allows you to quickly see the evolution of a file, including when changes were made, how large
they were, and who made them.
</p>
<p class="mb-0">
Use it to quickly find when the most impactful changes were made to a file or who best to talk
to about file changes and more.
</p>
</card-section>
<card-section dismissable id="worktrees">
<span slot="heading">Worktrees ✨</span>
<a
title="Learn more about Worktrees"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.worktrees%22"
><img
src="#{webroot}/media/plus-worktrees-illustrated.webp"
alt="Worktrees illustration"
class="plus-section-thumb mb-1"
/></a>
<p>
<a
title="Learn more about Worktrees"
href="command:gitlens.openWalkthrough?%22gitlens.plus%7Cgitlens.plus.worktrees%22"
>Worktrees</a
>
help you multitask by minimizing the context switching between branches, allowing you to easily
work on different branches of a repository simultaneously.
</p>
<p class="mb-0">
Avoid interrupting your work in progress when needing to review a pull request. Simply create a
new worktree and open it in a new VS Code window, all without impacting your other work
</p>
</card-section>
</div>
<div class="button-container">
<vscode-button appearance="secondary" data-action="command:workbench.action.toggleSidebarVisibility"
>Close</vscode-button
<nav class="nav-list">
<h3 class="nav-list__title t-eyebrow">Popular views</h3>
<a
class="nav-list__item"
href="command:gitlens.showGraph"
title="Show Commit Graph"
aria-label="Show Commit Graph"
><code-icon class="nav-list__icon" icon="gl-graph"></code-icon
><span class="nav-list__label">Commit Graph ✨</span></a
>
<a id="restore-welcome" class="link-minimal" href="command:gitlens.home.restoreWelcome"
>Restore Home view state</a
<a
class="nav-list__item"
href="command:gitlens.showCommitDetailsView"
title="Show Commit Details view"
aria-label="Show Commit Details view"
><code-icon class="nav-list__icon" icon="gl-commit-view"></code-icon
><span class="nav-list__label">Commit Details view</span></a
>
</div>
<a
class="nav-list__item"
href="command:gitlens.showFocusPage"
title="Open Focus"
aria-label="Open Focus"
><code-icon class="nav-list__icon" icon="target"></code-icon
><span class="nav-list__label">Focus ✨</span></a
>
<a
class="nav-list__item"
href="command:gitlens.showTimelineView"
title="Show Visual File History view"
aria-label="Show Visual File History view"
><code-icon class="nav-list__icon" icon="graph-scatter"></code-icon
><span class="nav-list__label">Visual File History ✨</span></a
>
</nav>
<nav class="nav-list">
<h3 class="nav-list__title t-eyebrow">Activity Bar</h3>
<a
class="nav-list__item"
href="command:workbench.view.extension.gitlens"
title="Show GitLens Side Bar"
aria-label="Show GitLens Side Bar"
><code-icon class="nav-list__icon" icon="gl-gitlens"></code-icon
><span class="nav-list__label">GitLens</span></a
>
<a
class="nav-list__item"
href="command:workbench.view.extension.gitlensInspect"
title="Show GitLens Inspect Side Bar"
aria-label="Show GitLens Inspect Side Bar"
><code-icon class="nav-list__icon" icon="gl-gitlens-inspect"></code-icon
><span class="nav-list__label">GitLens Inspect</span></a
>
<a
class="nav-list__item"
href="command:workbench.view.scm"
title="Show Source Control Side Bar"
aria-label="Show GitLens Side Bar"
><code-icon class="nav-list__icon" icon="source-control"></code-icon
><span class="nav-list__label">Source Control</span></a
>
</nav>
<nav class="nav-list">
<h3 class="nav-list__title t-eyebrow">Getting Started</h3>
<a
class="nav-list__item"
href="command:gitlens.showWelcomePage"
title="Open GitLens Welcome"
aria-label="Open GitLens Welcome"
><code-icon class="nav-list__icon" icon="play"></code-icon
><span class="nav-list__label">Welcome</span></a
>
<a
class="nav-list__item"
href="command:gitlens.getStarted"
title="Open GitLens Feature Walkthrough"
aria-label="Open GitLens Feature Walkthrough"
><code-icon class="nav-list__icon" icon="info"></code-icon
><span class="nav-list__label">Feature Walkthrough</span></a
>
<a
class="nav-list__item"
href="https://www.youtube.com/watch?v=UQPb73Zz9qk"
title="Open GitLens Settings"
aria-label="Open GitLens Settings"
><code-icon class="nav-list__icon" icon="vm-running"></code-icon
><span class="nav-list__label">Tutorial Video</span></a
>
<a
class="nav-list__item"
href="command:gitlens.showSettingsPage"
title="Open GitLens Settings"
aria-label="Open GitLens Settings"
><code-icon class="nav-list__icon" icon="gear"></code-icon
><span class="nav-list__label">GitLens Settings</span></a
>
</nav>
</main>
#{endOfBody}

+ 82
- 339
src/webviews/apps/home/home.scss View File

@ -1,3 +1,5 @@
@use '../shared/styles/properties';
:root {
--gitlens-z-inline: 1000;
--gitlens-z-sticky: 1100;
@ -121,81 +123,6 @@ body {
white-space: nowrap;
}
.home {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow: hidden;
&__header {
flex: none;
padding: 0 2rem;
position: relative;
[aria-hidden='false'] ~ header-card {
display: none;
}
}
&__main {
flex: 1;
overflow: auto;
padding: 2rem 2rem 0.4rem;
background: linear-gradient(var(--color-view-background) 33%, var(--color-view-background)),
linear-gradient(var(--color-view-background), var(--color-view-background) 66%) 0 100%,
linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)),
linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 0 100%;
background-color: var(--color-view-background);
background-repeat: no-repeat;
background-attachment: local, local, scroll, scroll;
background-size: 100% 12px, 100% 12px, 100% 6px, 100% 6px;
}
&__nav {
flex: none;
padding: 0 2rem;
margin-bottom: 0.6rem;
}
}
.popover {
background-color: var(--color-background--lighten-15);
position: absolute;
top: 100%;
left: 5.2rem;
transform: translateY(0.8rem);
max-width: 30rem;
padding: 0.8rem 1.2rem 1.2rem;
z-index: 10;
display: flex;
flex-direction: column;
gap: 0.4rem;
&__top {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
opacity: 0.5;
}
&__heading {
font-weight: 600;
}
&__caret {
position: absolute;
bottom: 100%;
width: 0;
height: 0;
border-left: 0.8rem solid transparent;
border-right: 0.8rem solid transparent;
border-bottom: 0.8rem solid var(--color-background--lighten-15);
}
}
h3 {
border: none;
color: var(--color-view-header-foreground);
@ -236,38 +163,41 @@ ul {
padding-left: 1.2em;
}
.unlist {
list-style: none;
padding-left: 0;
}
.icon-list {
list-style: none;
padding-left: 0;
.home {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow: hidden;
li {
&__header {
flex: none;
padding: 0 2rem;
position: relative;
padding-left: 2.2rem;
> code-icon:first-child {
position: absolute;
left: 0;
top: 0.1rem;
font-size: 1.6rem;
color: var(--color-foreground--50);
[aria-hidden='false'] ~ header-card {
display: none;
}
}
}
.button-container {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
&__main {
flex: 1;
overflow: auto;
padding: 0.8rem 2rem 0.4rem;
.button-link {
code-icon {
margin-right: 0.4rem;
background: linear-gradient(var(--color-view-background) 33%, var(--color-view-background)),
linear-gradient(var(--color-view-background), var(--color-view-background) 66%) 0 100%,
linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)),
linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 0 100%;
background-color: var(--color-view-background);
background-repeat: no-repeat;
background-attachment: local, local, scroll, scroll;
background-size: 100% 12px, 100% 12px, 100% 6px, 100% 6px;
}
&__nav {
flex: none;
padding: 0 2rem;
margin-bottom: 0.6rem;
}
}
@ -303,7 +233,7 @@ ul {
color: inherit;
border-radius: 0.3rem;
.codicon {
.code-icon {
line-height: 1.6rem;
}
@ -344,27 +274,6 @@ ul {
}
}
.gl-plus-banner {
background-color: transparent;
background-position: left -30vw center;
background-size: 80vw;
}
.plus-banner-text {
text-shadow: 0.1rem 0.1rem 0 var(--color-background), 0.1rem 0.1rem 0.2rem var(--color-background);
}
.logo {
font-size: 1.8rem;
color: var(--gitlens-brand-color-2);
font-weight: 500;
}
.description {
color: #b68cd8;
opacity: 0.6;
}
.alert {
padding: 0.8rem 1.2rem;
line-height: 1.2;
@ -399,250 +308,84 @@ ul {
}
}
.activitybar-banner {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
align-items: stretch;
gap: 1.6rem;
@media (max-width: 280px) {
flex-direction: column;
align-items: center;
}
ul {
display: flex;
flex-direction: column;
justify-content: center;
gap: clamp(0.1rem, 2vw, 1.2rem);
margin-bottom: 0;
}
&__content {
// padding-top: 1.6rem;
display: flex;
flex-direction: column;
justify-content: center;
> :last-child {
margin-bottom: 0;
}
}
&__media {
position: relative;
flex: none;
width: 9.2rem;
}
&__nav {
position: absolute;
top: 0;
left: 0.4rem;
width: 4.8rem;
height: 12.3rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 1.6rem;
&-item {
position: absolute;
left: 0.5rem;
width: 4.6rem;
height: 3.2rem;
// background-color: #ff000066;
&:first-of-type {
top: 2.2rem;
}
&:last-of-type {
top: 7rem;
}
}
}
gk-button {
max-width: 300px;
width: 100%;
#no-repo[aria-hidden='false'] ~ & {
display: none;
& + & {
margin-top: 1rem;
}
}
#no-repo {
.mb-0 {
margin-bottom: 0;
}
&[aria-hidden='true'] {
@media (max-width: 280px) {
.not-small {
display: none;
}
}
.video-banner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
margin-bottom: 0.8rem;
background: no-repeat var(--video-banner-play) 1.2rem center, no-repeat var(--video-banner-bg) left center;
background-color: var(--card-background);
background-size: clamp(2.9rem, 8%, 6rem), cover;
background-blend-mode: normal, overlay;
aspect-ratio: var(--video-banner-ratio, 354 / 54);
padding: 0.4rem 1.2rem;
color: inherit;
line-height: 1.2;
font-size: clamp(var(--vscode-font-size), 4vw, 2.4rem);
transition: aspect-ratio linear 100ms, background-color linear 100ms, background-position linear 200ms;
border-radius: 0.4rem;
@media (min-width: 277px) {
background-blend-mode: normal, normal;
background-position: center center, left center;
}
@media (min-width: 564px) {
aspect-ratio: var(--video-banner-ratio, 354 / 40);
}
&:hover {
background-color: var(--card-hover-background);
text-decoration: none;
color: inherit;
}
small {
color: #8d778d;
@media (min-width: 281px) {
.only-small {
display: none;
}
}
.link-minimal {
color: var(--color-foreground--50);
font-size: 1rem;
text-align: center;
position: relative;
top: 0.6rem;
&:hover {
.t {
&-eyebrow {
text-transform: uppercase;
font-size: 1rem;
font-weight: 600;
color: var(--color-foreground--50);
margin: 0;
}
}
vscode-button {
max-width: 300px;
width: 100%;
& + & {
margin-top: 1rem;
}
}
.link-minimal,
vscode-button {
align-self: center;
@media (min-width: 640px) {
align-self: flex-start;
.nav-list {
margin: {
left: -2rem;
right: -2rem;
}
}
@import '../shared/codicons';
// .codicon {
// position: relative;
// top: -2px;
// }
.type-tight {
line-height: 1.2;
}
.mb-1 {
margin-bottom: 0.4rem;
}
.mb-0 {
margin-bottom: 0;
}
.hide {
display: none;
}
.svg {
width: 100%;
height: auto;
&__outline {
transition: all ease 250ms;
.vscode-light &,
.vscode-high-contrast-light & {
stop-color: var(--color-background--darken-15);
}
display: flex;
flex-direction: column;
gap: 0.1rem;
align-items: stretch;
margin-bottom: 1.6rem;
.vscode-dark &,
.vscode-high-contrast & {
stop-color: var(--color-background--lighten-15);
}
}
&__item {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.8rem;
padding: 0.4rem 2rem;
color: inherit;
&:hover &__outline,
.activitybar-banner__nav-item:focus ~ & &__outline,
.activitybar-banner__nav-item:hover ~ & &__outline {
.vscode-light &,
.vscode-high-contrast-light & {
stop-color: var(--color-background--darken-50);
&:hover {
background-color: var(--vscode-list-hoverBackground);
color: var(--vscode-list-hoverForeground);
text-decoration: none;
}
.vscode-dark &,
.vscode-high-contrast & {
stop-color: var(--color-background--lighten-50);
&:active {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
}
&__bar {
fill: var(--vscode-activityBar-background);
}
&__indicator {
fill: transparent;
&.is-active {
fill: var(--vscode-activityBar-activeBorder);
}
}
&__icon {
transition: all ease 100ms;
fill: var(--vscode-activityBar-inactiveForeground);
&.is-active {
fill: var(--vscode-activityBar-foreground);
}
}
&__arrow {
fill: transparent;
&.is-active {
fill: var(--vscode-textLink-foreground);
}
opacity: 0.5;
}
.activitybar-banner__nav-item:first-of-type:focus ~ & &__icon:last-of-type,
.activitybar-banner__nav-item:first-of-type:hover ~ & &__icon:last-of-type,
.activitybar-banner__nav-item:last-of-type:focus ~ & &__icon:first-of-type,
.activitybar-banner__nav-item:last-of-type:hover ~ & &__icon:first-of-type {
fill: var(--vscode-activityBar-foreground);
&__label {
font-weight: 600;
}
}
.plus-section-thumb {
border-radius: 0.6rem;
}
@media (max-width: 280px) {
.not-small {
display: none;
&__item:hover &__label {
text-decoration: underline;
}
}
@media (min-width: 281px) {
.only-small {
display: none;
&__title {
padding: 0 2rem;
}
}

+ 5
- 289
src/webviews/apps/home/home.ts View File

@ -1,49 +1,21 @@
/*global*/
import './home.scss';
import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit';
import type { Disposable } from 'vscode';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../subscription';
import type { State } from '../../home/protocol';
import {
CompleteStepCommandType,
DidChangeConfigurationType,
DidChangeLayoutType,
DidChangeRepositoriesType,
DidChangeSubscriptionNotificationType,
DismissBannerCommandType,
DismissSectionCommandType,
DismissStatusCommandType,
} from '../../home/protocol';
import { DidChangeRepositoriesType } from '../../home/protocol';
import type { IpcMessage } from '../../protocol';
import { ExecuteCommandType, onIpc } from '../../protocol';
import { App } from '../shared/appBase';
import { DOM } from '../shared/dom';
import type { CardSection } from './components/card-section';
import type { HeaderCard } from './components/header-card';
import type { PlusBanner } from './components/plus-banner';
import type { SteppedSection } from './components/stepped-section';
import '../shared/components/button';
import '../shared/components/code-icon';
import '../shared/components/overlays/pop-over';
import './components/card-section';
import './components/header-card';
import './components/plus-banner';
import './components/plus-content';
import './components/stepped-section';
export class HomeApp extends App<State> {
private $steps!: SteppedSection[];
private $cards!: CardSection[];
constructor() {
super('HomeApp');
}
protected override onInitialize() {
provideVSCodeDesignSystem().register(vsCodeButton());
this.$steps = [...document.querySelectorAll<SteppedSection>('stepped-section[id]')];
this.$cards = [...document.querySelectorAll<CardSection>('card-section[id]')];
this.state = this.getState() ?? this.state;
this.updateState();
}
@ -54,29 +26,6 @@ export class HomeApp extends App {
disposables.push(
DOM.on('[data-action]', 'click', (e, target: HTMLElement) => this.onDataActionClicked(e, target)),
);
disposables.push(
DOM.on<PlusBanner, string>('plus-banner', 'action', (e, target: HTMLElement) =>
this.onPlusActionClicked(e, target),
),
);
disposables.push(
DOM.on<SteppedSection, boolean>('stepped-section', 'complete', (e, target: HTMLElement) =>
this.onStepComplete(e, target),
),
);
disposables.push(
DOM.on<CardSection, undefined>('card-section', 'dismiss', (e, target: HTMLElement) =>
this.onCardDismissed(e, target),
),
);
disposables.push(
DOM.on<HeaderCard, undefined>('header-card', 'dismiss-status', (e, target: HTMLElement) =>
this.onStatusDismissed(e, target),
),
);
disposables.push(
DOM.on('[data-banner-dismiss]', 'click', (e, target: HTMLElement) => this.onBannerDismissed(e, target)),
);
return disposables;
}
@ -85,18 +34,6 @@ export class HomeApp extends App {
const msg = e.data as IpcMessage;
switch (msg.method) {
case DidChangeSubscriptionNotificationType.method:
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeSubscriptionNotificationType, msg, params => {
this.state.subscription = params.subscription;
this.state.completedActions = params.completedActions;
this.state.avatar = params.avatar;
this.state.pinStatus = params.pinStatus;
this.setState(this.state);
this.updateState();
});
break;
case DidChangeRepositoriesType.method:
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
@ -106,154 +43,32 @@ export class HomeApp extends App {
this.updateNoRepo();
});
break;
case DidChangeConfigurationType.method:
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeConfigurationType, msg, params => {
this.state.plusEnabled = params.plusEnabled;
this.setState(this.state);
this.updatePlusContent();
});
break;
case DidChangeLayoutType.method:
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeLayoutType, msg, params => {
this.state.layout = params.layout;
this.setState(this.state);
this.updateLayout();
});
break;
default:
super.onMessageReceived?.(e);
break;
}
}
private onStepComplete(e: CustomEvent<boolean>, target: HTMLElement) {
const id = target.id;
const isComplete = e.detail ?? false;
this.state.completedSteps = toggleArrayItem(this.state.completedSteps, id, isComplete);
this.sendCommand(CompleteStepCommandType, { id: id, completed: isComplete });
this.updateState();
}
private onCardDismissed(e: CustomEvent<undefined>, target: HTMLElement) {
const id = target.id;
this.state.dismissedSections = toggleArrayItem(this.state.dismissedSections, id);
this.sendCommand(DismissSectionCommandType, { id: id });
this.updateState();
}
private onStatusDismissed(_e: CustomEvent<undefined>, _target: HTMLElement) {
this.state.pinStatus = false;
this.sendCommand(DismissStatusCommandType, undefined);
this.updateHeader();
}
private onBannerDismissed(_e: MouseEvent, target: HTMLElement) {
const key = target.getAttribute('data-banner-dismiss');
if (key == null || this.state.dismissedBanners?.includes(key)) {
return;
}
this.state.dismissedBanners = this.state.dismissedBanners ?? [];
this.state.dismissedBanners.push(key);
this.sendCommand(DismissBannerCommandType, { id: key });
this.updateBanners();
}
private onDataActionClicked(_e: MouseEvent, target: HTMLElement) {
const action = target.dataset.action;
this.onActionClickedCore(action);
}
private onPlusActionClicked(e: CustomEvent<string>, _target: HTMLElement) {
this.onActionClickedCore(e.detail);
}
private onActionClickedCore(action?: string) {
if (action?.startsWith('command:')) {
this.sendCommand(ExecuteCommandType, { command: action.slice(8) });
}
}
private getDaysRemaining() {
if (
![SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes(
this.state.subscription.state,
)
) {
return 0;
}
return getSubscriptionTimeRemaining(this.state.subscription, 'days') ?? 0;
}
private forceShowPlus() {
return [
SubscriptionState.FreePreviewTrialExpired,
SubscriptionState.FreePlusTrialExpired,
SubscriptionState.VerificationRequired,
].includes(this.state.subscription.state);
}
private updateHeader(days = this.getDaysRemaining(), forceShowPlus = this.forceShowPlus()) {
const { subscription, completedSteps, avatar, pinStatus } = this.state;
const $headerContent = document.getElementById('header-card') as HeaderCard;
if ($headerContent) {
if (avatar) {
$headerContent.setAttribute('image', avatar);
}
$headerContent.setAttribute('name', subscription.account?.name ?? '');
const steps = this.$steps?.length ?? 0;
let completed = completedSteps?.length ?? 0;
if (steps > 0 && completed > 0) {
const stepIds = this.$steps.map(el => el.id);
const availableCompleted = completedSteps!.filter(name => stepIds.includes(name));
completed = availableCompleted.length;
if (forceShowPlus && availableCompleted.includes('plus')) {
completed -= 1;
}
}
$headerContent.setAttribute('steps', steps.toString());
$headerContent.setAttribute('completed', completed.toString());
$headerContent.setAttribute('state', subscription.state.toString());
$headerContent.setAttribute('plan', subscription.plan.effective.name);
$headerContent.setAttribute('days', days.toString());
$headerContent.pinStatus = pinStatus;
}
}
private updateBanners() {
const $banners = [...document.querySelectorAll('[data-banner]')];
if (!$banners.length) {
return;
}
const { dismissedBanners } = this.state;
$banners.forEach($el => {
const key = $el.getAttribute('data-banner');
if (key !== null && dismissedBanners?.includes(key)) {
$el.setAttribute('hidden', 'true');
} else {
$el.removeAttribute('hidden');
}
});
}
private updateNoRepo() {
const {
repositories: { openCount, hasUnsafe, trusted },
} = this.state;
const header = document.getElementById('header')!;
if (!trusted) {
header.hidden = false;
setElementVisibility('untrusted-alert', true);
setElementVisibility('no-repo', false);
setElementVisibility('no-repo-alert', false);
setElementVisibility('unsafe-repo-alert', false);
@ -263,101 +78,13 @@ export class HomeApp extends App {
setElementVisibility('untrusted-alert', false);
const noRepos = openCount === 0;
setElementVisibility('no-repo', noRepos);
setElementVisibility('no-repo-alert', noRepos && !hasUnsafe);
setElementVisibility('unsafe-repo-alert', hasUnsafe);
}
private updateLayout() {
const { layout } = this.state;
const $els = [...document.querySelectorAll('[data-gitlens-layout]')];
$els.forEach(el => {
const attr = el.getAttribute('data-gitlens-layout');
el.classList.toggle('is-active', attr === layout);
});
}
private updatePlusContent(days = this.getDaysRemaining()) {
const { subscription, visibility, plusEnabled } = this.state;
let $plusContent = document.getElementById('plus-banner');
if ($plusContent) {
$plusContent.setAttribute('days', days.toString());
$plusContent.setAttribute('state', subscription.state.toString());
$plusContent.setAttribute('visibility', visibility);
$plusContent.setAttribute('plan', subscription.plan.effective.name);
$plusContent.setAttribute('plus', plusEnabled.toString());
($plusContent as PlusBanner).hasRepositories = this.state.repositories.count > 0;
}
$plusContent = document.getElementById('plus-content');
if ($plusContent) {
$plusContent.setAttribute('days', days.toString());
$plusContent.setAttribute('state', subscription.state.toString());
$plusContent.setAttribute('visibility', visibility);
$plusContent.setAttribute('plan', subscription.plan.effective.name);
}
}
private updateSteps(forceShowPlus = this.forceShowPlus()) {
if (
this.$steps == null ||
this.$steps.length === 0 ||
this.state.completedSteps == null ||
this.state.completedSteps.length === 0
) {
return;
}
this.$steps.forEach(el => {
el.setAttribute(
'completed',
(el.id === 'plus' && forceShowPlus) || this.state.completedSteps?.includes(el.id) !== true
? 'false'
: 'true',
);
});
}
private updateSections() {
if (
this.$cards == null ||
this.$cards.length === 0 ||
this.state.dismissedSections == null ||
this.state.dismissedSections.length === 0
) {
return;
}
this.state.dismissedSections.forEach(id => {
const found = this.$cards.findIndex(el => el.id === id);
if (found > -1) {
this.$cards[found].remove();
this.$cards.splice(found, 1);
}
});
header.hidden = !noRepos && !hasUnsafe;
}
private updateState() {
const { completedSteps, dismissedSections } = this.state;
this.updateNoRepo();
this.updateLayout();
const showRestoreWelcome = completedSteps?.length || dismissedSections?.length;
document.getElementById('restore-welcome')?.classList.toggle('hide', !showRestoreWelcome);
const forceShowPlus = this.forceShowPlus();
const days = this.getDaysRemaining();
this.updateHeader(days, forceShowPlus);
this.updatePlusContent(days);
this.updateSteps(forceShowPlus);
this.updateSections();
this.updateBanners();
}
}
@ -379,15 +106,4 @@ function setElementVisibility(elementOrId: string | HTMLElement | null | undefin
}
}
function toggleArrayItem(list: string[] = [], item: string, add = true) {
const hasStep = list.includes(item);
if (!hasStep && add) {
list.push(item);
} else if (hasStep && !add) {
list.splice(list.indexOf(item), 1);
}
return list;
}
new HomeApp();

+ 21
- 0
src/webviews/apps/plus/account/account.html View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body class="account preload" data-placement="#{placement}">
<header class="account__header">
<header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card>
</header>
#{endOfBody}
<style nonce="#{cspNonce}">
@font-face {
font-family: 'codicon';
font-display: block;
src: url('#{webroot}/codicon.ttf?0e5b0adf625a37fbcd638d31f0fe72aa') format('truetype');
}
</style>
</body>
</html>

+ 121
- 0
src/webviews/apps/plus/account/account.scss View File

@ -0,0 +1,121 @@
:root {
--gitlens-z-inline: 1000;
--gitlens-z-sticky: 1100;
--gitlens-z-popover: 1200;
--gitlens-z-cover: 1300;
--gitlens-z-dialog: 1400;
--gitlens-z-modal: 1500;
--gitlens-brand-color: #914db3;
--gitlens-brand-color-2: #a16dc4;
}
.vscode-high-contrast,
.vscode-dark {
--progress-bar-color: var(--color-background--lighten-15);
--card-background: var(--color-background--lighten-075);
--card-hover-background: var(--color-background--lighten-10);
--popover-bg: var(--color-background--lighten-15);
}
.vscode-high-contrast-light,
.vscode-light {
--progress-bar-color: var(--color-background--darken-15);
--card-background: var(--color-background--darken-075);
--card-hover-background: var(--color-background--darken-10);
--popover-bg: var(--color-background--darken-15);
}
* {
box-sizing: border-box;
}
// avoids FOUC for elements not yet called with `define()`
:not(:defined) {
visibility: hidden;
}
[hidden] {
display: none !important;
}
html {
height: 100%;
font-size: 62.5%;
text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: var(--color-view-background);
color: var(--color-view-foreground);
font-family: var(--font-family);
min-height: 100%;
line-height: 1.4;
font-size: var(--vscode-font-size);
&.scrollable,
.scrollable {
border-color: transparent;
transition: border-color 1s linear;
}
&:hover,
&:focus-within {
&.scrollable,
.scrollable {
border-color: var(--vscode-scrollbarSlider-background);
transition: none;
}
}
&.preload {
&.scrollable,
.scrollable {
transition: none;
}
}
}
::-webkit-scrollbar-corner {
background-color: transparent !important;
}
::-webkit-scrollbar-thumb {
background-color: transparent;
border-color: inherit;
border-right-style: inset;
border-right-width: calc(100vw + 100vh);
border-radius: unset !important;
&:hover {
border-color: var(--vscode-scrollbarSlider-hoverBackground);
}
&:active {
border-color: var(--vscode-scrollbarSlider-activeBackground);
}
}
:focus {
outline-color: var(--vscode-focusBorder);
}
.account {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow: hidden;
&__header {
flex: none;
padding: 0 2rem;
position: relative;
[aria-hidden='false'] ~ header-card {
display: none;
}
}
}

+ 105
- 0
src/webviews/apps/plus/account/account.ts View File

@ -0,0 +1,105 @@
/*global*/
import './account.scss';
import type { Disposable } from 'vscode';
import type { State } from '../../../../plus/webviews/account/protocol';
import { DidChangeSubscriptionNotificationType } from '../../../../plus/webviews/account/protocol';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription';
import type { IpcMessage } from '../../../protocol';
import { ExecuteCommandType, onIpc } from '../../../protocol';
import { App } from '../../shared/appBase';
import { DOM } from '../../shared/dom';
import type { HeaderCard } from './components/header-card';
import '../../shared/components/code-icon';
import './components/header-card';
export class AccountApp extends App<State> {
constructor() {
super('AccountApp');
}
protected override onInitialize() {
this.state = this.getState() ?? this.state;
this.updateState();
}
protected override onBind(): Disposable[] {
const disposables = super.onBind?.() ?? [];
disposables.push(
DOM.on('[data-action]', 'click', (e, target: HTMLElement) => this.onDataActionClicked(e, target)),
);
return disposables;
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
switch (msg.method) {
case DidChangeSubscriptionNotificationType.method:
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeSubscriptionNotificationType, msg, params => {
this.state.subscription = params.subscription;
this.state.avatar = params.avatar;
this.setState(this.state);
this.updateState();
});
break;
default:
super.onMessageReceived?.(e);
break;
}
}
private onDataActionClicked(_e: MouseEvent, target: HTMLElement) {
const action = target.dataset.action;
this.onActionClickedCore(action);
}
private onActionClickedCore(action?: string) {
if (action?.startsWith('command:')) {
this.sendCommand(ExecuteCommandType, { command: action.slice(8) });
}
}
private getDaysRemaining() {
if (
![SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes(
this.state.subscription.state,
)
) {
return 0;
}
return getSubscriptionTimeRemaining(this.state.subscription, 'days') ?? 0;
}
private updateHeader(days = this.getDaysRemaining()) {
const { subscription, avatar } = this.state;
const $headerContent = document.getElementById('header-card')! as HeaderCard;
if (avatar) {
$headerContent.setAttribute('image', avatar);
}
$headerContent.setAttribute('name', subscription.account?.name ?? '');
// TODO: remove
const steps = 0;
const completed = 0;
$headerContent.setAttribute('steps', steps.toString());
$headerContent.setAttribute('completed', completed.toString());
$headerContent.setAttribute('state', subscription.state.toString());
$headerContent.setAttribute('plan', subscription.plan.effective.name);
$headerContent.setAttribute('days', days.toString());
}
private updateState() {
const days = this.getDaysRemaining();
this.updateHeader(days);
}
}
new AccountApp();

src/webviews/apps/home/components/header-card.ts → src/webviews/apps/plus/account/components/header-card.ts View File

@ -1,9 +1,8 @@
import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element';
import { SubscriptionState } from '../../../../subscription';
import { pluralize } from '../../../../system/string';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/code-icon';
import '../../shared/components/overlays/pop-over';
import { SubscriptionState } from '../../../../../subscription';
import { pluralize } from '../../../../../system/string';
import { numberConverter } from '../../../shared/components/converters/number-converter';
import '../../../shared/components/code-icon';
const template = html<HeaderCard>`
<div class="header-card__media"><img class="header-card__image" src="${x => x.image}" alt="GitLens Logo" /></div>
@ -17,29 +16,6 @@ const template = html`
><span class="repo-access${x => (x.isPro ? ' is-pro' : '')}"></span>${x =>
`${x.planName}${x.daysLeft}`}</span
>
<pop-over class="${x => (x.pinStatus ? 'is-pinned' : null)}">
${when(
x => x.pinStatus,
html<HeaderCard>`
<span slot="type">${x => x.planName}</span>
<a
href="#"
class="action is-icon"
slot="actions"
@click="${(x, c) => x.dismissStatus(c.event as MouseEvent)}"
title="Dismiss"
aria-label="Dismiss"
><code-icon icon="close"></code-icon
></a>
`,
)}
${x =>
x.isPro
? 'You have access to all GitLens and GitLens+ features on any repo.'
: 'You have access to GitLens+ features on local & public repos, and all other GitLens features on any repo.'}
<br /><br />
indicates GitLens+ features, <a class="link-inline" href="command:gitlens.plus.learn">learn more</a>
</pop-over>
</span>
<span class="account-actions">
${when(
@ -66,12 +42,22 @@ const template = html`
)}
</span>
</p>
<p class="features">
${x =>
x.isPro
? 'You have access to all GitLens features on any repo.'
: 'You have access to ✨ features on local & public repos, and all other GitLens features on any repo.'}
<br /><br />
indicates a subscription is required to use this feature on privately hosted repos.
<a class="link-inline" href="command:gitlens.plus.learn">learn more</a>
</p>
<div
class="progress header-card__progress"
role="progressbar"
aria-valuemax="${x => x.progressMax}"
aria-valuenow="${x => x.progressNow}"
aria-label="${x => x.progressNow} of ${x => x.progressMax} steps completed"
hidden
>
<div ${ref('progressNode')} class="progress__indicator"></div>
</div>
@ -118,11 +104,6 @@ const styles = css`
:host {
position: relative;
display: grid;
/*
padding: 1rem 1rem 1.2rem;
background-color: var(--card-background);
border-radius: 0.4rem;
*/
padding: 1rem 0 1.2rem;
gap: 0 0.8rem;
grid-template-columns: 3.4rem auto;
@ -175,6 +156,11 @@ const styles = css`
gap: 0 0.4rem;
}
.features {
grid-column: 1 / 3;
grid-row: 3;
}
.progress {
width: 100%;
overflow: hidden;
@ -199,10 +185,6 @@ const styles = css`
position: absolute;
bottom: 0;
left: 0;
/*
border-bottom-left-radius: 0.4rem;
border-bottom-right-radius: 0.4rem;
*/
}
.brand {
@ -212,18 +194,6 @@ const styles = css`
color: var(--color-foreground--65);
}
.status-label {
cursor: help;
}
.status pop-over {
top: 1.6em;
left: 0;
}
.status-label:not(:hover) + pop-over:not(.is-pinned) {
display: none;
}
.repo-access {
font-size: 1.1em;
margin-right: 0.2rem;
@ -277,10 +247,6 @@ const styles = css`
background-color: var(--color-background--darken-10);
}
pop-over .action {
margin-right: -0.2rem;
}
.link-inline {
color: inherit;
text-decoration: underline;

src/webviews/apps/home/components/plus-content.ts → src/webviews/apps/plus/account/components/plus-content.ts View File

@ -1,8 +1,8 @@
import { attr, css, customElement, FASTElement, html, volatile, when } from '@microsoft/fast-element';
import { SubscriptionState } from '../../../../subscription';
import { pluralize } from '../../../../system/string';
import { numberConverter } from '../../shared/components/converters/number-converter';
import '../../shared/components/code-icon';
import { SubscriptionState } from '../../../../../subscription';
import { pluralize } from '../../../../../system/string';
import { numberConverter } from '../../../shared/components/converters/number-converter';
import '../../../shared/components/code-icon';
const template = html<PlusContent>`
<div class="icon"><code-icon icon="info"></code-icon></div>

+ 3
- 3
src/webviews/apps/shared/components/account/account-badge.ts View File

@ -15,7 +15,7 @@ const template = html`
<pop-over placement="${x => x.placement}" class="badge-popover">
${x => x.popoverText}
<br /><br />
indicates GitLens+ features
indicates a subscription is required to use this feature on privately hosted repos.
</pop-over>
</template>
`;
@ -138,7 +138,7 @@ export class AccountBadge extends FASTElement {
@volatile
get popoverText() {
return this.isPro
? 'You have access to all GitLens and GitLens+ features on any repo.'
: 'You have access to GitLens+ features on local & public repos, and all other GitLens features on any repo.';
? 'You have access to all GitLens features on any repo.'
: 'You have access to features on local & public repos, and all other GitLens features on any repo.';
}
}

+ 7
- 282
src/webviews/home/homeWebview.ts View File

@ -1,38 +1,9 @@
import type { ConfigurationChangeEvent } from 'vscode';
import { Disposable, window, workspace } from 'vscode';
import { getAvatarUriFromGravatarEmail } from '../../avatars';
import { ViewsLayout } from '../../commands/setViewsLayout';
import { Disposable, workspace } from 'vscode';
import type { Container } from '../../container';
import type { RepositoriesVisibility } from '../../git/gitProviderService';
import type { SubscriptionChangeEvent } from '../../plus/subscription/subscriptionService';
import type { Subscription } from '../../subscription';
import { executeCoreCommand, registerCommand } from '../../system/command';
import { configuration } from '../../system/configuration';
import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function';
import { getSettledValue } from '../../system/promise';
import type { StorageChangeEvent } from '../../system/storage';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import { registerCommand } from '../../system/command';
import type { WebviewController, WebviewProvider } from '../webviewController';
import type {
CompleteStepParams,
DidChangeRepositoriesParams,
DismissBannerParams,
DismissSectionParams,
State,
} from './protocol';
import {
CompletedActions,
CompleteStepCommandType,
DidChangeConfigurationType,
DidChangeLayoutType,
DidChangeRepositoriesType,
DidChangeSubscriptionNotificationType,
DismissBannerCommandType,
DismissSectionCommandType,
DismissStatusCommandType,
} from './protocol';
import type { DidChangeRepositoriesParams, State } from './protocol';
import { DidChangeRepositoriesType } from './protocol';
const emptyDisposable = Object.freeze({
dispose: () => {
@ -45,10 +16,7 @@ export class HomeWebviewProvider implements WebviewProvider {
constructor(private readonly container: Container, private readonly host: WebviewController<State>) {
this._disposable = Disposable.from(
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this),
this.container.storage.onDidChange(this.onStorageChanged, this),
!workspace.isTrusted
? workspace.onDidGrantWorkspaceTrust(this.notifyDidChangeRepositories, this)
: emptyDisposable,
@ -59,220 +27,23 @@ export class HomeWebviewProvider implements WebviewProvider {
this._disposable.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (!configuration.changed(e, 'plusFeatures.enabled')) {
return;
}
this.notifyDidChangeConfiguration();
}
private onRepositoriesChanged() {
this.notifyDidChangeRepositories();
}
private onStorageChanged(e: StorageChangeEvent) {
if (e.key !== 'views:layout') return;
this.notifyDidChangeLayout();
}
private async onSubscriptionChanged(e: SubscriptionChangeEvent) {
await this.container.storage.store('home:status:pinned', true);
void this.notifyDidChangeData(e.current);
}
onVisibilityChanged(visible: boolean): void {
if (!visible) {
this._validateSubscriptionDebounced?.cancel();
return;
}
queueMicrotask(() => void this.validateSubscription());
}
onWindowFocusChanged(focused: boolean): void {
if (!focused || !this.host.visible) {
this._validateSubscriptionDebounced?.cancel();
return;
}
queueMicrotask(() => void this.validateSubscription());
}
registerCommands(): Disposable[] {
return [
registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this),
registerCommand('gitlens.home.toggleWelcome', async () => {
const welcomeVisible = !this.welcomeVisible;
await this.container.storage.store('views:welcome:visible', welcomeVisible);
if (welcomeVisible) {
await Promise.allSettled([
this.container.storage.store('home:actions:completed', []),
this.container.storage.store('home:steps:completed', []),
this.container.storage.store('home:sections:dismissed', []),
]);
}
void this.host.refresh();
}),
registerCommand('gitlens.home.restoreWelcome', async () => {
await Promise.allSettled([
this.container.storage.store('home:steps:completed', []),
this.container.storage.store('home:sections:dismissed', []),
]);
void this.host.refresh();
}),
registerCommand('gitlens.home.showSCM', async () => {
const completedActions = this.container.storage.get('home:actions:completed', []);
if (!completedActions.includes(CompletedActions.OpenedSCM)) {
completedActions.push(CompletedActions.OpenedSCM);
await this.container.storage.store('home:actions:completed', completedActions);
void this.notifyDidChangeData();
}
await executeCoreCommand('workbench.view.scm');
}),
];
}
onMessageReceived(e: IpcMessage) {
switch (e.method) {
case CompleteStepCommandType.method:
onIpc(CompleteStepCommandType, e, params => this.completeStep(params));
break;
case DismissSectionCommandType.method:
onIpc(DismissSectionCommandType, e, params => this.dismissSection(params));
break;
case DismissStatusCommandType.method:
onIpc(DismissStatusCommandType, e, _params => this.dismissPinStatus());
break;
case DismissBannerCommandType.method:
onIpc(DismissBannerCommandType, e, params => this.dismissBanner(params));
break;
}
}
private async completeStep({ id, completed = false }: CompleteStepParams) {
const steps = this.container.storage.get('home:steps:completed', []);
const hasStep = steps.includes(id);
if (!hasStep && completed) {
steps.push(id);
} else if (hasStep && !completed) {
steps.splice(steps.indexOf(id), 1);
}
await this.container.storage.store('home:steps:completed', steps);
void this.notifyDidChangeData();
}
private async dismissSection(params: DismissSectionParams) {
const sections = this.container.storage.get('home:sections:dismissed', []);
if (sections.includes(params.id)) return;
sections.push(params.id);
await this.container.storage.store('home:sections:dismissed', sections);
void this.notifyDidChangeData();
}
private async dismissBanner(params: DismissBannerParams) {
const banners = this.container.storage.get('home:banners:dismissed', []);
if (!banners.includes(params.id)) {
banners.push(params.id);
}
await this.container.storage.store('home:banners:dismissed', banners);
void this.notifyDidChangeData();
}
private async dismissPinStatus() {
await this.container.storage.store('home:status:pinned', false);
void this.notifyDidChangeData();
return [registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this)];
}
includeBootstrap(): Promise<State> {
return this.getState();
}
private get welcomeVisible(): boolean {
return this.container.storage.get('views:welcome:visible', true);
}
private async getRepoVisibility(): Promise<RepositoriesVisibility> {
const visibility = await this.container.git.visibility();
return visibility;
}
private async getSubscription(subscription?: Subscription) {
// Make sure to make a copy of the array otherwise it will be live to the storage value
const completedActions = [...this.container.storage.get('home:actions:completed', [])];
if (!this.welcomeVisible) {
completedActions.push(CompletedActions.DismissedWelcome);
}
const sub = subscription ?? (await this.container.subscription.getSubscription(true));
let avatar;
if (sub.account?.email) {
avatar = getAvatarUriFromGravatarEmail(sub.account.email, 34).toString();
} else {
avatar = `${this.host.getWebRoot() ?? ''}/media/gitlens-logo.webp`;
}
return {
subscription: sub,
completedActions: completedActions,
avatar: avatar,
};
}
private getPinStatus() {
return this.container.storage.get('home:status:pinned') ?? true;
}
private async getState(subscription?: Subscription): Promise<State> {
const [visibilityResult, subscriptionResult] = await Promise.allSettled([
this.getRepoVisibility(),
this.getSubscription(subscription),
]);
const sub = getSettledValue(subscriptionResult)!;
const steps = this.container.storage.get('home:steps:completed', []);
const sections = this.container.storage.get('home:sections:dismissed', []);
const dismissedBanners = this.container.storage.get('home:banners:dismissed', []);
return {
private async getState(): Promise<State> {
return Promise.resolve({
timestamp: Date.now(),
repositories: this.getRepositoriesState(),
webroot: this.host.getWebRoot(),
subscription: sub.subscription,
completedActions: sub.completedActions,
plusEnabled: this.getPlusEnabled(),
visibility: getSettledValue(visibilityResult)!,
completedSteps: steps,
dismissedSections: sections,
avatar: sub.avatar,
layout: this.getLayout(),
pinStatus: this.getPinStatus(),
dismissedBanners: dismissedBanners,
};
}
private notifyDidChangeData(subscription?: Subscription) {
if (!this.host.ready) return false;
return window.withProgress({ location: { viewId: this.host.id } }, async () => {
const sub = await this.getSubscription(subscription);
return this.host.notify(DidChangeSubscriptionNotificationType, {
...sub,
pinStatus: this.getPinStatus(),
});
});
}
@ -290,50 +61,4 @@ export class HomeWebviewProvider implements WebviewProvider {
void this.host.notify(DidChangeRepositoriesType, this.getRepositoriesState());
}
private getPlusEnabled() {
return configuration.get('plusFeatures.enabled');
}
private notifyDidChangeConfiguration() {
if (!this.host.ready) return;
void this.host.notify(DidChangeConfigurationType, {
plusEnabled: this.getPlusEnabled(),
});
}
private getLayout() {
const layout = this.container.storage.get('views:layout');
return layout != null ? (layout as ViewsLayout) : ViewsLayout.SourceControl;
}
private notifyDidChangeLayout() {
if (!this.host.ready) return;
void this.host.notify(DidChangeLayoutType, { layout: this.getLayout() });
}
private _validateSubscriptionDebounced: Deferrable<HomeWebviewProvider['validateSubscription']> | undefined =
undefined;
private async validateSubscription(): Promise<void> {
if (this._validateSubscriptionDebounced == null) {
this._validateSubscriptionDebounced = debounce(this.validateSubscriptionCore, 1000);
}
await this._validateSubscriptionDebounced();
}
private _validating: Promise<void> | undefined;
private async validateSubscriptionCore() {
if (this._validating == null) {
this._validating = this.container.subscription.validate();
try {
await this._validating;
} finally {
this._validating = undefined;
}
}
}
}

+ 1
- 54
src/webviews/home/protocol.ts View File

@ -1,7 +1,4 @@
import type { ViewsLayout } from '../../commands/setViewsLayout';
import type { RepositoriesVisibility } from '../../git/gitProviderService';
import type { Subscription } from '../../subscription';
import { IpcCommandType, IpcNotificationType } from '../protocol';
import { IpcNotificationType } from '../protocol';
export const enum CompletedActions {
DismissedWelcome = 'dismissed:welcome',
@ -13,46 +10,8 @@ export interface State {
repositories: DidChangeRepositoriesParams;
webroot?: string;
subscription: Subscription;
completedActions: CompletedActions[];
completedSteps?: string[];
dismissedBanners?: string[];
dismissedSections?: string[];
plusEnabled: boolean;
visibility: RepositoriesVisibility;
avatar?: string;
layout: ViewsLayout;
pinStatus: boolean;
}
export interface CompleteStepParams {
id: string;
completed: boolean;
}
export const CompleteStepCommandType = new IpcCommandType<CompleteStepParams>('home/step/complete');
export interface DismissSectionParams {
id: string;
}
export const DismissSectionCommandType = new IpcCommandType<DismissSectionParams>('home/section/dismiss');
export const DismissStatusCommandType = new IpcCommandType<undefined>('home/status/dismiss');
export interface DismissBannerParams {
id: string;
}
export const DismissBannerCommandType = new IpcCommandType<DismissBannerParams>('home/banner/dismiss');
export interface DidChangeSubscriptionParams {
subscription: Subscription;
completedActions: CompletedActions[];
avatar?: string;
pinStatus: boolean;
}
export const DidChangeSubscriptionNotificationType = new IpcNotificationType<DidChangeSubscriptionParams>(
'subscription/didChange',
);
export interface DidChangeRepositoriesParams {
count: number;
openCount: number;
@ -60,15 +19,3 @@ export interface DidChangeRepositoriesParams {
trusted: boolean;
}
export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');
export interface DidChangeConfigurationParams {
plusEnabled: boolean;
}
export const DidChangeConfigurationType = new IpcNotificationType<DidChangeConfigurationParams>(
'configuration/didChange',
);
export interface DidChangeLayoutParams {
layout: ViewsLayout;
}
export const DidChangeLayoutType = new IpcNotificationType<DidChangeLayoutParams>('layout/didChange');

+ 2
- 0
webpack.config.js View File

@ -329,6 +329,7 @@ function getWebviewsConfig(mode, env) {
getHtmlPlugin('timeline', true, mode, env),
getHtmlPlugin('welcome', false, mode, env),
getHtmlPlugin('focus', true, mode, env),
getHtmlPlugin('account', true, mode, env),
getCspHtmlPlugin(mode, env),
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
new CopyPlugin({
@ -392,6 +393,7 @@ function getWebviewsConfig(mode, env) {
timeline: './plus/timeline/timeline.ts',
welcome: './welcome/welcome.ts',
focus: './plus/focus/focus.ts',
account: './plus/account/account.ts',
},
mode: mode,
target: 'web',

Loading…
Cancel
Save