diff --git a/apps/desktop/cypress.config.ts b/apps/desktop/cypress.config.ts index b6e68a4c5e..9b0e4076bf 100644 --- a/apps/desktop/cypress.config.ts +++ b/apps/desktop/cypress.config.ts @@ -1,4 +1,6 @@ +import { svelte } from '@sveltejs/vite-plugin-svelte'; import { defineConfig } from 'cypress'; +import path from 'path'; export default defineConfig({ retries: { @@ -7,9 +9,29 @@ export default defineConfig({ // Configure retry attempts for `cypress open` openMode: 0 }, + e2e: { baseUrl: 'http://localhost:1420', supportFile: 'cypress/e2e/support/index.ts' }, - experimentalWebKitSupport: true + + experimentalWebKitSupport: true, + + component: { + devServer: { + framework: 'svelte', + bundler: 'vite', + viteConfig: { + plugins: [svelte()], + resolve: { + alias: { + $components: path.resolve('src/components'), + $lib: path.resolve('src/lib') + } + } + } + }, + // 👇 And this line if Cypress still fails to resolve the iframe mount file + indexHtmlFile: 'cypress/support/index.html' + } }); diff --git a/apps/desktop/cypress/component/MesageEditor.cy.ts b/apps/desktop/cypress/component/MesageEditor.cy.ts new file mode 100644 index 0000000000..80f2a4195b --- /dev/null +++ b/apps/desktop/cypress/component/MesageEditor.cy.ts @@ -0,0 +1,38 @@ +import MessageEditor from '$components/v3/editor/MessageEditor.svelte'; +import { SETTINGS, type Settings } from '$lib/settings/userSettings'; +import { UiState } from '$lib/state/uiState.svelte'; +import { TestId } from '$lib/testing/testIds'; +import { HttpClient } from '@gitbutler/shared/network/httpClient'; +import { UploadsService } from '@gitbutler/shared/uploads/uploadsService'; +import { readable, writable } from 'svelte/store'; +import '../../src/styles/styles.css'; +import '@gitbutler/ui/main.css'; + +describe('CommitMesageEditor.cy.ts', () => { + const httpClient = new HttpClient(window.fetch, 'https://www.example.com', writable('')); + const settings = writable({} as Settings); + it('playground', () => { + const context = new Map(); + const uiState = new UiState(readable({ ids: [], entities: {} }), () => {}); + context.set(UiState, uiState); + context.set(UploadsService, new UploadsService(httpClient)); + context.set(SETTINGS, settings); + + const mountResult = cy.mount(MessageEditor, { + props: { + projectId: '1234', + initialValue: 'Hello world!', + placeholder: 'text goes here', + testId: TestId.EditCommitMessageBox + } as const, + context + }); + mountResult + .then(async ({ component }) => { + const comp = component as MessageEditor; + return await comp.getPlaintext(); + }) + .should('eq', 'Hello world!'); + cy.getByTestId(TestId.EditCommitMessageBox).should('exist').click().type('new text!'); + }); +}); diff --git a/apps/desktop/cypress/support/commands.ts b/apps/desktop/cypress/support/commands.ts new file mode 100644 index 0000000000..26ff8270bd --- /dev/null +++ b/apps/desktop/cypress/support/commands.ts @@ -0,0 +1,49 @@ +/// + +import type { TestId } from '$lib/testing/testIds'; + +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } + +type TestIdValues = `${TestId}`; + +Cypress.Commands.add('getByTestId', (testId: TestIdValues, containingText?: string) => { + if (containingText) { + return cy.contains(`[data-testid="${testId}"]`, containingText, { timeout: 15000 }); + } + return cy.get(`[data-testid="${testId}"]`, { timeout: 15000 }); +}); diff --git a/apps/desktop/cypress/support/component.ts b/apps/desktop/cypress/support/component.ts new file mode 100644 index 0000000000..0230a656ef --- /dev/null +++ b/apps/desktop/cypress/support/component.ts @@ -0,0 +1,36 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +import { mount } from 'cypress/svelte'; + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount; + } + } +} + +Cypress.Commands.add('mount', mount); + +// Example use: +// cy.mount(MyComponent) diff --git a/apps/desktop/cypress/support/index.html b/apps/desktop/cypress/support/index.html new file mode 100644 index 0000000000..ac6e79fd83 --- /dev/null +++ b/apps/desktop/cypress/support/index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/apps/desktop/cypress/tsconfig.json b/apps/desktop/cypress/tsconfig.json index 43681fb925..b8cdcdcbd3 100644 --- a/apps/desktop/cypress/tsconfig.json +++ b/apps/desktop/cypress/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": ["cypress"] }, - "include": ["e2e/**/*.ts"] + "include": ["**/*.ts"] } diff --git a/apps/desktop/src/components/v3/editor/MessageEditor.svelte b/apps/desktop/src/components/v3/editor/MessageEditor.svelte index 3db67dce98..a804d7efe2 100644 --- a/apps/desktop/src/components/v3/editor/MessageEditor.svelte +++ b/apps/desktop/src/components/v3/editor/MessageEditor.svelte @@ -15,6 +15,7 @@ import { showError } from '$lib/notifications/toasts'; import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import { UiState } from '$lib/state/uiState.svelte'; + import { TestId } from '$lib/testing/testIds'; import { getContext, getContextStoreBySymbol } from '@gitbutler/shared/context'; import { uploadFiles } from '@gitbutler/shared/dom'; import { persisted } from '@gitbutler/shared/persisted'; @@ -54,9 +55,9 @@ enableSmiles?: boolean; enableRichText?: boolean; enableRuler?: boolean; - onAiButtonClick: (params: AiButtonClickParams) => void; - canUseAI: boolean; - aiIsLoading: boolean; + onAiButtonClick?: (params: AiButtonClickParams) => void; + canUseAI?: boolean; + aiIsLoading?: boolean; suggestionsHandler?: CommitSuggestions; testId?: string; } @@ -219,7 +220,7 @@ function handleGenerateMessage() { if (aiIsLoading) return; - onAiButtonClick({ + onAiButtonClick?.({ useEmojiStyle: $commitGenerationUseEmojis, useBriefStyle: $commitGenerationExtraConcise }); @@ -385,6 +386,7 @@ onclick={() => { useFloatingBox.current = !useFloatingBox.current; }} + testId={TestId.FloatingModeButton} />
{#if enableSmiles} diff --git a/apps/desktop/src/lib/analytics/analytics.ts b/apps/desktop/src/lib/analytics/analytics.ts index 3f838f1cbb..1e4c1bbcc5 100644 --- a/apps/desktop/src/lib/analytics/analytics.ts +++ b/apps/desktop/src/lib/analytics/analytics.ts @@ -3,6 +3,7 @@ import { initSentry } from '$lib/analytics/sentry'; import { AppSettings } from '$lib/config/appSettings'; import { getName, getVersion } from '@tauri-apps/api/app'; import posthog from 'posthog-js'; +import { PUBLIC_POSTHOG_API_KEY } from '$env/static/public'; export function initAnalyticsIfEnabled(appSettings: AppSettings, postHog: PostHogWrapper) { if (import.meta.env.MODE === 'development') return; @@ -15,7 +16,7 @@ export function initAnalyticsIfEnabled(appSettings: AppSettings, postHog: PostHo appSettings.appMetricsEnabled.onDisk().then(async (enabled) => { if (enabled) { const [appName, appVersion] = await Promise.all([getName(), getVersion()]); - postHog.init(appName, appVersion); + postHog.init(appName, appVersion, PUBLIC_POSTHOG_API_KEY); } }); appSettings.appNonAnonMetricsEnabled.onDisk().then((enabled) => { diff --git a/apps/desktop/src/lib/analytics/posthog.ts b/apps/desktop/src/lib/analytics/posthog.ts index 105fd1dda4..726c4cd2ec 100644 --- a/apps/desktop/src/lib/analytics/posthog.ts +++ b/apps/desktop/src/lib/analytics/posthog.ts @@ -2,7 +2,6 @@ import { PostHog, posthog, type Properties } from 'posthog-js'; import type { EventContext } from '$lib/analytics/eventContext'; import type { SettingsService } from '$lib/config/appSettingsV2'; import type { RepoInfo } from '$lib/url/gitUrl'; -import { PUBLIC_POSTHOG_API_KEY } from '$env/static/public'; export class PostHogWrapper { private _instance: PostHog | void = undefined; @@ -18,8 +17,8 @@ export class PostHogWrapper { this._instance?.capture(eventName, newProperties); } - async init(appName: string, appVersion: string) { - this._instance = posthog.init(PUBLIC_POSTHOG_API_KEY, { + async init(appName: string, appVersion: string, apiKey: string) { + this._instance = posthog.init(apiKey, { api_host: 'https://eu.posthog.com', autocapture: false, disable_session_recording: true, diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index df857c60c2..9ad12a9e81 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -2,6 +2,8 @@ import { invoke as invokeIpc, listen as listenIpc } from '$lib/backend/ipc'; import { getVersion } from '@tauri-apps/api/app'; import { check } from '@tauri-apps/plugin-updater'; +export const IS_TAURI_ENV = '__TAURI_INTERNALS__' in window; + export class Tauri { invoke = invokeIpc; listen = listenIpc; diff --git a/apps/desktop/src/lib/irc/ircService.svelte.ts b/apps/desktop/src/lib/irc/ircService.svelte.ts index 3c01e6ebd4..9638596314 100644 --- a/apps/desktop/src/lib/irc/ircService.svelte.ts +++ b/apps/desktop/src/lib/irc/ircService.svelte.ts @@ -25,7 +25,7 @@ import persistReducer from 'redux-persist/es/persistReducer'; import storage from 'redux-persist/lib/storage'; import type { IrcClient } from '$lib/irc/ircClient.svelte'; import type { IrcEvent } from '$lib/irc/parser'; -import type { IrcChannel, IrcChat, WhoInfo } from '$lib/irc/types'; +import type { IrcChannel, IrcChat, IRCState, WhoInfo } from '$lib/irc/types'; import type { ClientState } from '$lib/state/clientState.svelte'; import type { Reactive } from '@gitbutler/shared/storeUtils'; import type { ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'; @@ -49,32 +49,25 @@ export class IrcService { }; clientState.inject(ircSlice.reducerPath, persistReducer(persistConfig, ircSlice.reducer)); + const store = clientState.rootState; - $effect(() => { - if (clientState.reactiveState) { - if (ircSlice.reducerPath in clientState.reactiveState) { - // @ts-expect-error code-splitting means it's not defined in client state. - this.state = clientState.reactiveState[ircSlice.reducerPath] as IRCState; - } - } + store.subscribe((value) => { + // @ts-expect-error code-splitting means it's not defined in client state. + this.state = value[ircSlice.reducerPath] as IRCState; }); - $effect(() => { - return this.ircClient.onevent(async (event) => { - return this.handleEvent(event); - }); + this.ircClient.onevent(async (event) => { + return this.handleEvent(event); }); - $effect(() => { - return this.ircClient.onopen(() => { - const channels = this.getChannels(); - this.dispatch(clearNames()); - setTimeout(() => { - for (const channel of channels.current) { - this.send(`JOIN ${channel?.name}`); - } - }, 5000); - }); + this.ircClient.onopen(() => { + const channels = this.getChannels(); + this.dispatch(clearNames()); + setTimeout(() => { + for (const channel of channels.current) { + this.send(`JOIN ${channel?.name}`); + } + }, 5000); }); } diff --git a/apps/desktop/src/lib/platform/platform.ts b/apps/desktop/src/lib/platform/platform.ts index 8d52f72c9f..c6f9cf1d4b 100644 --- a/apps/desktop/src/lib/platform/platform.ts +++ b/apps/desktop/src/lib/platform/platform.ts @@ -1,3 +1,4 @@ +import { IS_TAURI_ENV } from '$lib/backend/tauri'; import { platform } from '@tauri-apps/plugin-os'; -export const platformName = platform(); +export const platformName = IS_TAURI_ENV ? platform() : undefined; diff --git a/apps/desktop/src/lib/selection/uncommittedService.svelte.ts b/apps/desktop/src/lib/selection/uncommittedService.svelte.ts index 8af2803b06..49977d131b 100644 --- a/apps/desktop/src/lib/selection/uncommittedService.svelte.ts +++ b/apps/desktop/src/lib/selection/uncommittedService.svelte.ts @@ -47,10 +47,10 @@ export class UncommittedService { persistReducer(persistConfig, uncommittedSlice.reducer) ); - $effect(() => { - if (clientState.reactiveState && uncommittedSlice.reducerPath in clientState.reactiveState) { + clientState.rootState.subscribe((value) => { + if (value && uncommittedSlice.reducerPath in value) { // @ts-expect-error code-splitting means it's not defined in client state. - this.state = clientState.reactiveState[uncommittedSlice.reducerPath]; + this.state = value[uncommittedSlice.reducerPath]; } }); } diff --git a/apps/desktop/src/lib/state/clientState.svelte.ts b/apps/desktop/src/lib/state/clientState.svelte.ts index a283591702..c582646510 100644 --- a/apps/desktop/src/lib/state/clientState.svelte.ts +++ b/apps/desktop/src/lib/state/clientState.svelte.ts @@ -2,12 +2,12 @@ import { tauriBaseQuery } from '$lib/state/backendQuery'; import { butlerModule } from '$lib/state/butlerModule'; import { ReduxTag } from '$lib/state/tags'; import { uiStateSlice } from '$lib/state/uiState.svelte'; -import { mergeUnlisten } from '@gitbutler/ui/utils/mergeUnlisten'; import { combineSlices, configureStore, type Reducer } from '@reduxjs/toolkit'; import { buildCreateApi, coreModule, setupListeners, type RootState } from '@reduxjs/toolkit/query'; import { FLUSH, PAUSE, PERSIST, persistReducer, PURGE, REGISTER, REHYDRATE } from 'redux-persist'; import persistStore from 'redux-persist/lib/persistStore'; import storage from 'redux-persist/lib/storage'; +import { derived, writable, type Readable } from 'svelte/store'; import type { PostHogWrapper } from '$lib/analytics/posthog'; import type { Tauri } from '$lib/backend/tauri'; import type { GitHubClient } from '$lib/forge/github/githubClient'; @@ -43,8 +43,8 @@ export class ClientState { // $state requires field declaration, but we have to assign the initial // value in the constructor such that we can inject dependencies. The // incorrect casting `as` seems difficult to avoid. - rootState = $state.raw({} as ReturnType); - readonly uiState = $derived(this.rootState.uiState); + readonly rootState = writable({} as ReturnType); + readonly uiState = derived(this.rootState, (value) => value.uiState); /** rtk-query api for communicating with the back end. */ readonly backendApi: BackendApi; @@ -55,10 +55,6 @@ export class ClientState { /** rtk-query api for communicating with GitLab. */ readonly gitlabApi: GitLabApi; - get reactiveState() { - return this.rootState; - } - constructor( tauri: Tauri, gitHubClient: GitHubClient, @@ -69,7 +65,7 @@ export class ClientState { const butlerMod = butlerModule({ // Reactive loop without nested function. // TODO: Can it be done without nesting? - getState: () => () => this.rootState as any as RootState, + store: this.rootState as any as Readable>, getDispatch: () => this.dispatch, posthog }); @@ -91,16 +87,13 @@ export class ClientState { this.reducer = reducer; setupListeners(this.store.dispatch); this.dispatch = this.store.dispatch; - this.rootState = this.store.getState(); - - $effect(() => - mergeUnlisten( - this.store.subscribe(() => { - this.rootState = this.store.getState(); - }), - setupListeners(this.store.dispatch) - ) - ); + this.rootState.set(this.store.getState()); + + this.store.subscribe(() => { + this.rootState.set(this.store.getState()); + }); + + setupListeners(this.store.dispatch); } inject(reducerPath: string, reducer: Reducer) { diff --git a/apps/desktop/src/lib/state/context.ts b/apps/desktop/src/lib/state/context.ts index 04387c62b5..8d504006bd 100644 --- a/apps/desktop/src/lib/state/context.ts +++ b/apps/desktop/src/lib/state/context.ts @@ -1,6 +1,7 @@ import type { PostHogWrapper } from '$lib/analytics/posthog'; import type { EntityState, ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'; import type { CombinedState } from '@reduxjs/toolkit/query'; +import type { Readable } from 'svelte/store'; /** * The api is necessary to create the store, so we need to provide @@ -11,7 +12,7 @@ import type { CombinedState } from '@reduxjs/toolkit/query'; */ export type HookContext = { /** Without the nested function we get looping reactivity. */ - getState: () => () => { [k: string]: CombinedState | EntityState }; + store: Readable<{ [k: string]: CombinedState | EntityState }>; getDispatch: () => ThunkDispatch; posthog?: PostHogWrapper; }; diff --git a/apps/desktop/src/lib/state/customHooks.svelte.ts b/apps/desktop/src/lib/state/customHooks.svelte.ts index cb0a135c4a..bf50913064 100644 --- a/apps/desktop/src/lib/state/customHooks.svelte.ts +++ b/apps/desktop/src/lib/state/customHooks.svelte.ts @@ -37,14 +37,18 @@ const EVENT_NAME = 'tauri_command'; export function buildQueryHooks({ api, endpointName, - ctx: { getState, getDispatch } + ctx: { store, getDispatch } }: { api: Api; endpointName: string; ctx: HookContext; }) { const endpoint = api.endpoints[endpointName]!; - const state = getState() as any as () => RootState; + let state = $state.raw>({}); + + store.subscribe((value) => { + state = value as RootState; + }); const { initiate, select } = endpoint as ApiEndpointQuery, Definitions>; @@ -94,7 +98,7 @@ export function buildQueryHooks({ } const selector = $derived(select(queryArg)); - const result = $derived(selector(state())); + const result = $derived(selector(state)); const output = $derived.by(() => { let data = result.data; if (options?.transform && data) { @@ -133,7 +137,7 @@ export function buildQueryHooks({ const results = queryArgs.map((queryArg) => { const selector = $derived(select(queryArg)); - const result = $derived(selector(state())); + const result = $derived(selector(state)); const output = $derived.by(() => { let data = result.data; if (options?.transform && data) { @@ -151,7 +155,7 @@ export function buildQueryHooks({ function useQueryState(queryArg: unknown, options?: { transform?: T }) { const selector = $derived(select(queryArg)); - const result = $derived(selector(state())); + const result = $derived(selector(state)); const output = $derived.by(() => { let data = result.data; if (options?.transform && data) { @@ -167,7 +171,7 @@ export function buildQueryHooks({ function useQueryTimeStamp(queryArg: unknown) { const selector = $derived(select(queryArg)); - const result = $derived(selector(state())); + const result = $derived(selector(state)); return reactive(() => result.startedTimeStamp); } @@ -283,7 +287,7 @@ export function buildMutationHook< endpointName, actionName, command, - ctx: { getState, getDispatch, posthog } + ctx: { store: getState, getDispatch, posthog } }: { api: Api; endpointName: string; @@ -292,7 +296,10 @@ export function buildMutationHook< ctx: HookContext; }): MutationHook { const endpoint = api.endpoints[endpointName]!; - const state = getState() as any as () => RootState; + let state = $state.raw>({}); + getState.subscribe((value) => { + state = value as RootState; + }); const { initiate, select } = endpoint as unknown as ApiEndpointMutation; @@ -398,7 +405,7 @@ export function buildMutationHook< } const selector = $derived(select({ requestId: promise?.requestId, fixedCacheKey })); - const result = $derived(selector(state())); + const result = $derived(selector(state)); $effect(() => { return () => { diff --git a/apps/desktop/src/lib/state/uiState.svelte.ts b/apps/desktop/src/lib/state/uiState.svelte.ts index aeadf36b48..10170fde52 100644 --- a/apps/desktop/src/lib/state/uiState.svelte.ts +++ b/apps/desktop/src/lib/state/uiState.svelte.ts @@ -9,6 +9,7 @@ import { type UnknownAction } from '@reduxjs/toolkit'; import type { RejectionReason } from '$lib/stacks/stackService.svelte'; +import type { Readable } from 'svelte/store'; export type StackSelection = { branchName: string; @@ -138,11 +139,11 @@ export class UiState { }); constructor( - reactiveState: Reactive, + store: Readable, private dispatch: ThunkDispatch ) { - $effect(() => { - this.state = reactiveState.current; + store.subscribe((value) => { + this.state = value; }); } diff --git a/apps/desktop/src/lib/testing/mockGitHubApi.svelte.ts b/apps/desktop/src/lib/testing/mockGitHubApi.svelte.ts index 7d65c3068c..c42dba22c5 100644 --- a/apps/desktop/src/lib/testing/mockGitHubApi.svelte.ts +++ b/apps/desktop/src/lib/testing/mockGitHubApi.svelte.ts @@ -4,6 +4,7 @@ import { butlerModule } from '$lib/state/butlerModule'; import { createGitHubApi } from '$lib/state/clientState.svelte'; import { Octokit } from '@octokit/rest'; import { configureStore, type ThunkDispatch, type UnknownAction } from '@reduxjs/toolkit'; +import { readable } from 'svelte/store'; /** * Mock for GitHub RTKQ. @@ -31,7 +32,7 @@ export function setupMockGitHubApi() { const gitHubClient = new GitHubClient({ client: octokit }); gitHubClient.setRepo({ owner: 'test-owner', repo: 'test-repo' }); const gitHubApi = createGitHubApi( - butlerModule({ getDispatch: () => dispatch!, getState: () => () => state }) + butlerModule({ getDispatch: () => dispatch!, store: readable(state) }) ); const store = configureStore({ diff --git a/apps/desktop/src/lib/testing/testIds.ts b/apps/desktop/src/lib/testing/testIds.ts index 552dd0a128..f31ccbb037 100644 --- a/apps/desktop/src/lib/testing/testIds.ts +++ b/apps/desktop/src/lib/testing/testIds.ts @@ -104,7 +104,8 @@ export enum TestId { StackSelectionView = 'stack-selection-view', BranchesSelectionView = 'branches-selection-view', WorkspaceSelectionView = 'workspace-selection-view', - ProjectNotFoundPage = 'project-not-found-page' + ProjectNotFoundPage = 'project-not-found-page', + FloatingModeButton = 'floating-mode-button' } export enum ElementId { diff --git a/apps/desktop/src/lib/utils/theme.ts b/apps/desktop/src/lib/utils/theme.ts index 45bfe297ff..2518bb38f9 100644 --- a/apps/desktop/src/lib/utils/theme.ts +++ b/apps/desktop/src/lib/utils/theme.ts @@ -1,17 +1,19 @@ +import { IS_TAURI_ENV } from '$lib/backend/tauri'; import { getCurrentWindow, type Theme } from '@tauri-apps/api/window'; import { type Writable } from 'svelte/store'; import type { Settings } from '$lib/settings/userSettings'; -const appWindow = getCurrentWindow(); + +const appWindow = IS_TAURI_ENV ? getCurrentWindow() : undefined; let systemTheme: string | null; let selectedTheme: string | undefined; export function initTheme(userSettings: Writable) { - appWindow.theme().then((value: Theme | null) => { + appWindow?.theme().then((value: Theme | null) => { systemTheme = value; updateDom(); }); - appWindow.onThemeChanged((e) => { + appWindow?.onThemeChanged((e) => { systemTheme = e.payload; updateDom(); }); diff --git a/apps/desktop/src/routes/+layout.svelte b/apps/desktop/src/routes/+layout.svelte index 30317757b5..c083eca57c 100644 --- a/apps/desktop/src/routes/+layout.svelte +++ b/apps/desktop/src/routes/+layout.svelte @@ -78,7 +78,6 @@ import { ProjectService as CloudProjectService } from '@gitbutler/shared/organizations/projectService'; import { RepositoryIdLookupService } from '@gitbutler/shared/organizations/repositoryIdLookupService'; import { PatchCommitService as CloudPatchCommitService } from '@gitbutler/shared/patches/patchCommitService'; - import { reactive } from '@gitbutler/shared/reactiveUtils.svelte'; import { AppDispatch, AppState } from '@gitbutler/shared/redux/store.svelte'; import { WebRoutesService } from '@gitbutler/shared/routing/webRoutes.svelte'; import { UploadsService } from '@gitbutler/shared/uploads/uploadsService'; @@ -142,11 +141,7 @@ projectMetrics: data.projectMetrics }); - const uiStateSlice = $derived(clientState.uiState); - const uiState = new UiState( - reactive(() => uiStateSlice), - clientState.dispatch - ); + const uiState = new UiState(clientState.uiState, clientState.dispatch); setContext(UiState, uiState); const intelligentScrollingService = new IntelligentScrollingService(uiState); setContext(IntelligentScrollingService, intelligentScrollingService); diff --git a/apps/desktop/src/routes/+layout.ts b/apps/desktop/src/routes/+layout.ts index 0c93eeae98..d8da80491e 100644 --- a/apps/desktop/src/routes/+layout.ts +++ b/apps/desktop/src/routes/+layout.ts @@ -4,7 +4,7 @@ import { initAnalyticsIfEnabled } from '$lib/analytics/analytics'; import { EventContext } from '$lib/analytics/eventContext'; import { PostHogWrapper } from '$lib/analytics/posthog'; import { CommandService } from '$lib/backend/ipc'; -import { Tauri } from '$lib/backend/tauri'; +import { IS_TAURI_ENV, Tauri } from '$lib/backend/tauri'; import { loadAppSettings } from '$lib/config/appSettings'; import { SettingsService } from '$lib/config/appSettingsV2'; import { GitConfigService } from '$lib/config/gitConfigService'; @@ -37,7 +37,9 @@ export const csr = true; export const load: LayoutLoad = async () => { // TODO: Find a workaround to avoid this dynamic import // https://github.com/sveltejs/kit/issues/905 - const defaultPath = await (await import('@tauri-apps/api/path')).homeDir(); + const defaultPath = IS_TAURI_ENV + ? await (await import('@tauri-apps/api/path')).homeDir() + : undefined; const commandService = new CommandService(); diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index b77455bae3..37cae037a8 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -30,6 +30,5 @@ "src/**/*.ts", "src/**/*.svelte", "cypress.config.ts", - "cypress/**/*.ts" ] } diff --git a/packages/ui/src/styles/utility/layout.min.css b/packages/ui/src/styles/utility/layout.min.css index 65da54c768..2a6c487429 100644 --- a/packages/ui/src/styles/utility/layout.min.css +++ b/packages/ui/src/styles/utility/layout.min.css @@ -1,11 +1,566 @@ /* Spasing */ -.gap-2{gap:2px}.p-2{padding:2px}.p-left-2{padding-left:2px}.p-right-2{padding-right:2px}.p-top-2{padding-top:2px}.p-bottom-2{padding-bottom:2px}.m-2{margin:2px}.m-left-2{margin-left:2px}.m-right-2{margin-right:2px}.m-top-2{margin-top:2px}.m-bottom-2{margin-bottom:2px}.gap-4{gap:4px}.p-4{padding:4px}.p-left-4{padding-left:4px}.p-right-4{padding-right:4px}.p-top-4{padding-top:4px}.p-bottom-4{padding-bottom:4px}.m-4{margin:4px}.m-left-4{margin-left:4px}.m-right-4{margin-right:4px}.m-top-4{margin-top:4px}.m-bottom-4{margin-bottom:4px}.gap-6{gap:6px}.p-6{padding:6px}.p-left-6{padding-left:6px}.p-right-6{padding-right:6px}.p-top-6{padding-top:6px}.p-bottom-6{padding-bottom:6px}.m-6{margin:6px}.m-left-6{margin-left:6px}.m-right-6{margin-right:6px}.m-top-6{margin-top:6px}.m-bottom-6{margin-bottom:6px}.gap-8{gap:8px}.p-8{padding:8px}.p-left-8{padding-left:8px}.p-right-8{padding-right:8px}.p-top-8{padding-top:8px}.p-bottom-8{padding-bottom:8px}.m-8{margin:8px}.m-left-8{margin-left:8px}.m-right-8{margin-right:8px}.m-top-8{margin-top:8px}.m-bottom-8{margin-bottom:8px}.gap-10{gap:10px}.p-10{padding:10px}.p-left-10{padding-left:10px}.p-right-10{padding-right:10px}.p-top-10{padding-top:10px}.p-bottom-10{padding-bottom:10px}.m-10{margin:10px}.m-left-10{margin-left:10px}.m-right-10{margin-right:10px}.m-top-10{margin-top:10px}.m-bottom-10{margin-bottom:10px}.gap-12{gap:12px}.p-12{padding:12px}.p-left-12{padding-left:12px}.p-right-12{padding-right:12px}.p-top-12{padding-top:12px}.p-bottom-12{padding-bottom:12px}.m-12{margin:12px}.m-left-12{margin-left:12px}.m-right-12{margin-right:12px}.m-top-12{margin-top:12px}.m-bottom-12{margin-bottom:12px}.gap-14{gap:14px}.p-14{padding:14px}.p-left-14{padding-left:14px}.p-right-14{padding-right:14px}.p-top-14{padding-top:14px}.p-bottom-14{padding-bottom:14px}.m-14{margin:14px}.m-left-14{margin-left:14px}.m-right-14{margin-right:14px}.m-top-14{margin-top:14px}.m-bottom-14{margin-bottom:14px}.gap-16{gap:16px}.p-16{padding:16px}.p-left-16{padding-left:16px}.p-right-16{padding-right:16px}.p-top-16{padding-top:16px}.p-bottom-16{padding-bottom:16px}.m-16{margin:16px}.m-left-16{margin-left:16px}.m-right-16{margin-right:16px}.m-top-16{margin-top:16px}.m-bottom-16{margin-bottom:16px}.gap-20{gap:20px}.p-20{padding:20px}.p-left-20{padding-left:20px}.p-right-20{padding-right:20px}.p-top-20{padding-top:20px}.p-bottom-20{padding-bottom:20px}.m-20{margin:20px}.m-left-20{margin-left:20px}.m-right-20{margin-right:20px}.m-top-20{margin-top:20px}.m-bottom-20{margin-bottom:20px}.gap-24{gap:24px}.p-24{padding:24px}.p-left-24{padding-left:24px}.p-right-24{padding-right:24px}.p-top-24{padding-top:24px}.p-bottom-24{padding-bottom:24px}.m-24{margin:24px}.m-left-24{margin-left:24px}.m-right-24{margin-right:24px}.m-top-24{margin-top:24px}.m-bottom-24{margin-bottom:24px}.gap-28{gap:28px}.p-28{padding:28px}.p-left-28{padding-left:28px}.p-right-28{padding-right:28px}.p-top-28{padding-top:28px}.p-bottom-28{padding-bottom:28px}.m-28{margin:28px}.m-left-28{margin-left:28px}.m-right-28{margin-right:28px}.m-top-28{margin-top:28px}.m-bottom-28{margin-bottom:28px}.gap-32{gap:32px}.p-32{padding:32px}.p-left-32{padding-left:32px}.p-right-32{padding-right:32px}.p-top-32{padding-top:32px}.p-bottom-32{padding-bottom:32px}.m-32{margin:32px}.m-left-32{margin-left:32px}.m-right-32{margin-right:32px}.m-top-32{margin-top:32px}.m-bottom-32{margin-bottom:32px}.gap-36{gap:36px}.p-36{padding:36px}.p-left-36{padding-left:36px}.p-right-36{padding-right:36px}.p-top-36{padding-top:36px}.p-bottom-36{padding-bottom:36px}.m-36{margin:36px}.m-left-36{margin-left:36px}.m-right-36{margin-right:36px}.m-top-36{margin-top:36px}.m-bottom-36{margin-bottom:36px}.gap-40{gap:40px}.p-40{padding:40px}.p-left-40{padding-left:40px}.p-right-40{padding-right:40px}.p-top-40{padding-top:40px}.p-bottom-40{padding-bottom:40px}.m-40{margin:40px}.m-left-40{margin-left:40px}.m-right-40{margin-right:40px}.m-top-40{margin-top:40px}.m-bottom-40{margin-bottom:40px}.gap-44{gap:44px}.p-44{padding:44px}.p-left-44{padding-left:44px}.p-right-44{padding-right:44px}.p-top-44{padding-top:44px}.p-bottom-44{padding-bottom:44px}.m-44{margin:44px}.m-left-44{margin-left:44px}.m-right-44{margin-right:44px}.m-top-44{margin-top:44px}.m-bottom-44{margin-bottom:44px}.gap-48{gap:48px}.p-48{padding:48px}.p-left-48{padding-left:48px}.p-right-48{padding-right:48px}.p-top-48{padding-top:48px}.p-bottom-48{padding-bottom:48px}.m-48{margin:48px}.m-left-48{margin-left:48px}.m-right-48{margin-right:48px}.m-top-48{margin-top:48px}.m-bottom-48{margin-bottom:48px} +.gap-2 { + gap: 2px; +} +.p-2 { + padding: 2px; +} +.p-left-2 { + padding-left: 2px; +} +.p-right-2 { + padding-right: 2px; +} +.p-top-2 { + padding-top: 2px; +} +.p-bottom-2 { + padding-bottom: 2px; +} +.m-2 { + margin: 2px; +} +.m-left-2 { + margin-left: 2px; +} +.m-right-2 { + margin-right: 2px; +} +.m-top-2 { + margin-top: 2px; +} +.m-bottom-2 { + margin-bottom: 2px; +} +.gap-4 { + gap: 4px; +} +.p-4 { + padding: 4px; +} +.p-left-4 { + padding-left: 4px; +} +.p-right-4 { + padding-right: 4px; +} +.p-top-4 { + padding-top: 4px; +} +.p-bottom-4 { + padding-bottom: 4px; +} +.m-4 { + margin: 4px; +} +.m-left-4 { + margin-left: 4px; +} +.m-right-4 { + margin-right: 4px; +} +.m-top-4 { + margin-top: 4px; +} +.m-bottom-4 { + margin-bottom: 4px; +} +.gap-6 { + gap: 6px; +} +.p-6 { + padding: 6px; +} +.p-left-6 { + padding-left: 6px; +} +.p-right-6 { + padding-right: 6px; +} +.p-top-6 { + padding-top: 6px; +} +.p-bottom-6 { + padding-bottom: 6px; +} +.m-6 { + margin: 6px; +} +.m-left-6 { + margin-left: 6px; +} +.m-right-6 { + margin-right: 6px; +} +.m-top-6 { + margin-top: 6px; +} +.m-bottom-6 { + margin-bottom: 6px; +} +.gap-8 { + gap: 8px; +} +.p-8 { + padding: 8px; +} +.p-left-8 { + padding-left: 8px; +} +.p-right-8 { + padding-right: 8px; +} +.p-top-8 { + padding-top: 8px; +} +.p-bottom-8 { + padding-bottom: 8px; +} +.m-8 { + margin: 8px; +} +.m-left-8 { + margin-left: 8px; +} +.m-right-8 { + margin-right: 8px; +} +.m-top-8 { + margin-top: 8px; +} +.m-bottom-8 { + margin-bottom: 8px; +} +.gap-10 { + gap: 10px; +} +.p-10 { + padding: 10px; +} +.p-left-10 { + padding-left: 10px; +} +.p-right-10 { + padding-right: 10px; +} +.p-top-10 { + padding-top: 10px; +} +.p-bottom-10 { + padding-bottom: 10px; +} +.m-10 { + margin: 10px; +} +.m-left-10 { + margin-left: 10px; +} +.m-right-10 { + margin-right: 10px; +} +.m-top-10 { + margin-top: 10px; +} +.m-bottom-10 { + margin-bottom: 10px; +} +.gap-12 { + gap: 12px; +} +.p-12 { + padding: 12px; +} +.p-left-12 { + padding-left: 12px; +} +.p-right-12 { + padding-right: 12px; +} +.p-top-12 { + padding-top: 12px; +} +.p-bottom-12 { + padding-bottom: 12px; +} +.m-12 { + margin: 12px; +} +.m-left-12 { + margin-left: 12px; +} +.m-right-12 { + margin-right: 12px; +} +.m-top-12 { + margin-top: 12px; +} +.m-bottom-12 { + margin-bottom: 12px; +} +.gap-14 { + gap: 14px; +} +.p-14 { + padding: 14px; +} +.p-left-14 { + padding-left: 14px; +} +.p-right-14 { + padding-right: 14px; +} +.p-top-14 { + padding-top: 14px; +} +.p-bottom-14 { + padding-bottom: 14px; +} +.m-14 { + margin: 14px; +} +.m-left-14 { + margin-left: 14px; +} +.m-right-14 { + margin-right: 14px; +} +.m-top-14 { + margin-top: 14px; +} +.m-bottom-14 { + margin-bottom: 14px; +} +.gap-16 { + gap: 16px; +} +.p-16 { + padding: 16px; +} +.p-left-16 { + padding-left: 16px; +} +.p-right-16 { + padding-right: 16px; +} +.p-top-16 { + padding-top: 16px; +} +.p-bottom-16 { + padding-bottom: 16px; +} +.m-16 { + margin: 16px; +} +.m-left-16 { + margin-left: 16px; +} +.m-right-16 { + margin-right: 16px; +} +.m-top-16 { + margin-top: 16px; +} +.m-bottom-16 { + margin-bottom: 16px; +} +.gap-20 { + gap: 20px; +} +.p-20 { + padding: 20px; +} +.p-left-20 { + padding-left: 20px; +} +.p-right-20 { + padding-right: 20px; +} +.p-top-20 { + padding-top: 20px; +} +.p-bottom-20 { + padding-bottom: 20px; +} +.m-20 { + margin: 20px; +} +.m-left-20 { + margin-left: 20px; +} +.m-right-20 { + margin-right: 20px; +} +.m-top-20 { + margin-top: 20px; +} +.m-bottom-20 { + margin-bottom: 20px; +} +.gap-24 { + gap: 24px; +} +.p-24 { + padding: 24px; +} +.p-left-24 { + padding-left: 24px; +} +.p-right-24 { + padding-right: 24px; +} +.p-top-24 { + padding-top: 24px; +} +.p-bottom-24 { + padding-bottom: 24px; +} +.m-24 { + margin: 24px; +} +.m-left-24 { + margin-left: 24px; +} +.m-right-24 { + margin-right: 24px; +} +.m-top-24 { + margin-top: 24px; +} +.m-bottom-24 { + margin-bottom: 24px; +} +.gap-28 { + gap: 28px; +} +.p-28 { + padding: 28px; +} +.p-left-28 { + padding-left: 28px; +} +.p-right-28 { + padding-right: 28px; +} +.p-top-28 { + padding-top: 28px; +} +.p-bottom-28 { + padding-bottom: 28px; +} +.m-28 { + margin: 28px; +} +.m-left-28 { + margin-left: 28px; +} +.m-right-28 { + margin-right: 28px; +} +.m-top-28 { + margin-top: 28px; +} +.m-bottom-28 { + margin-bottom: 28px; +} +.gap-32 { + gap: 32px; +} +.p-32 { + padding: 32px; +} +.p-left-32 { + padding-left: 32px; +} +.p-right-32 { + padding-right: 32px; +} +.p-top-32 { + padding-top: 32px; +} +.p-bottom-32 { + padding-bottom: 32px; +} +.m-32 { + margin: 32px; +} +.m-left-32 { + margin-left: 32px; +} +.m-right-32 { + margin-right: 32px; +} +.m-top-32 { + margin-top: 32px; +} +.m-bottom-32 { + margin-bottom: 32px; +} +.gap-36 { + gap: 36px; +} +.p-36 { + padding: 36px; +} +.p-left-36 { + padding-left: 36px; +} +.p-right-36 { + padding-right: 36px; +} +.p-top-36 { + padding-top: 36px; +} +.p-bottom-36 { + padding-bottom: 36px; +} +.m-36 { + margin: 36px; +} +.m-left-36 { + margin-left: 36px; +} +.m-right-36 { + margin-right: 36px; +} +.m-top-36 { + margin-top: 36px; +} +.m-bottom-36 { + margin-bottom: 36px; +} +.gap-40 { + gap: 40px; +} +.p-40 { + padding: 40px; +} +.p-left-40 { + padding-left: 40px; +} +.p-right-40 { + padding-right: 40px; +} +.p-top-40 { + padding-top: 40px; +} +.p-bottom-40 { + padding-bottom: 40px; +} +.m-40 { + margin: 40px; +} +.m-left-40 { + margin-left: 40px; +} +.m-right-40 { + margin-right: 40px; +} +.m-top-40 { + margin-top: 40px; +} +.m-bottom-40 { + margin-bottom: 40px; +} +.gap-44 { + gap: 44px; +} +.p-44 { + padding: 44px; +} +.p-left-44 { + padding-left: 44px; +} +.p-right-44 { + padding-right: 44px; +} +.p-top-44 { + padding-top: 44px; +} +.p-bottom-44 { + padding-bottom: 44px; +} +.m-44 { + margin: 44px; +} +.m-left-44 { + margin-left: 44px; +} +.m-right-44 { + margin-right: 44px; +} +.m-top-44 { + margin-top: 44px; +} +.m-bottom-44 { + margin-bottom: 44px; +} +.gap-48 { + gap: 48px; +} +.p-48 { + padding: 48px; +} +.p-left-48 { + padding-left: 48px; +} +.p-right-48 { + padding-right: 48px; +} +.p-top-48 { + padding-top: 48px; +} +.p-bottom-48 { + padding-bottom: 48px; +} +.m-48 { + margin: 48px; +} +.m-left-48 { + margin-left: 48px; +} +.m-right-48 { + margin-right: 48px; +} +.m-top-48 { + margin-top: 48px; +} +.m-bottom-48 { + margin-bottom: 48px; +} /* Position */ -.relative { position: relative; } .absolute { position: absolute; } .fixed { position: fixed; } .sticky { position: sticky; } .top-0 { top: 0; } .bottom-0 { bottom: 0; } .left-0 { left: 0; } .right-0 { right: 0; } +.relative { + position: relative; +} +.absolute { + position: absolute; +} +.fixed { + position: fixed; +} +.sticky { + position: sticky; +} +.top-0 { + top: 0; +} +.bottom-0 { + bottom: 0; +} +.left-0 { + left: 0; +} +.right-0 { + right: 0; +} /* Size */ -.full-width { width: 100%; } .full-height { height: 100%; } +.full-width { + width: 100%; +} +.full-height { + height: 100%; +} /* Flexbox */ .flex { @@ -29,9 +584,6 @@ .full-width { width: 100%; } -.full-height { - height: 100%; -} .flex-1 { flex: 1; }