From d5c81cba4e431fae38c9264f2363bdfa230228c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20R=C3=B8ssum?= <1959615+joglr@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:47:16 +0100 Subject: [PATCH 1/6] Run eslint and prettier in workflows (#728) This pull request adds the necessary steps to run eslint and prettier in the workflows. Fixes #727 --- .eslintrc.cjs | 80 ++++++++++++++++++++++ .eslintrc.json | 17 ----- .github/workflows/check-bun.yml | 49 +++++++++++++ .github/workflows/check-node.yml | 48 +++++++++++++ .github/workflows/test-and-build.yml | 36 ---------- package-lock.json | 13 ++-- package.json | 12 ++-- playwright.config.ts | 3 +- prettier.config.mjs | 9 +++ src/analyzer/analyze.server.ts | 10 ++- src/analyzer/hydrate.server.ts | 6 +- src/components/Chart.tsx | 11 +-- src/components/CommitHistory.tsx | 13 ++-- src/components/FeedbackCard.tsx | 12 ++-- src/components/HiddenFiles.tsx | 14 ++-- src/components/LoadingIndicator.tsx | 2 +- src/components/Tooltip.tsx | 19 +++-- src/components/UnionAuthorsModal.tsx | 8 +-- src/components/accordion/AccordionItem.tsx | 6 +- src/contexts/CommitTabContext.ts | 4 +- src/metrics/singleAuthor.ts | 20 ++++-- src/root.tsx | 8 +-- src/routes/$repo.$.tsx | 10 +-- 23 files changed, 281 insertions(+), 129 deletions(-) create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.json create mode 100644 .github/workflows/check-bun.yml create mode 100644 .github/workflows/check-node.yml delete mode 100644 .github/workflows/test-and-build.yml create mode 100644 prettier.config.mjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 000000000..43b261043 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,80 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true + } + }, + env: { + browser: true, + commonjs: true, + node: true, + es6: true + }, + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended" + ], + settings: { + react: { + version: "detect" + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" } + ], + "import/resolver": { + typescript: {} + } + } + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"] + }, + typescript: { + alwaysTryTypes: true + } + } + }, + extends: ["plugin:@typescript-eslint/recommended", "plugin:import/recommended", "plugin:import/typescript"] + }, + + // Node + { + files: [".eslintrc.js"], + env: { + node: true + } + } + ] +} diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index ffe1705b1..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "env": { - "browser": true, - "node": true - }, - "extends": ["@remix-run/eslint-config", "eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "no-case-declarations": "off" - }, - "ignorePatterns": ["node_modules", "dist", "build", "public/build"] -} diff --git a/.github/workflows/check-bun.yml b/.github/workflows/check-bun.yml new file mode 100644 index 000000000..707691b89 --- /dev/null +++ b/.github/workflows/check-bun.yml @@ -0,0 +1,49 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of bun + +name: Check (bun) + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check-bun: + runs-on: ubuntu-latest + + strategy: + matrix: + bun-version: [1.0.0] + # See Bun releases at https://github.com/oven-sh/bun/releases + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: "0" + + - name: Use Bun ${{ matrix.bun-version }} + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ matrix.bun-version }} + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test src + + - name: Build application + run: bun run build + + - name: Run typecheck + run: bun run typecheck + + - name: Check linting + run: bun run lint + + - name: Check formatting + run: bun run format:check + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 diff --git a/.github/workflows/check-node.yml b/.github/workflows/check-node.yml new file mode 100644 index 000000000..44f9f8f38 --- /dev/null +++ b/.github/workflows/check-node.yml @@ -0,0 +1,48 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of bun + +name: Check (node) + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check-node: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x, 18.x] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: "0" + + - name: Use Node ${{ matrix.bun-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Run unit tests + run: npm run test:unit + + - name: Build application + run: npm run build + + - name: Run typecheck + run: npm run typecheck + + - name: Check linting + run: npm run lint + + - name: Check formatting + run: npm run format:check + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml deleted file mode 100644 index 13f9e9b4c..000000000 --- a/.github/workflows/test-and-build.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of bun - -name: Test and build - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test-and-build: - runs-on: ubuntu-latest - - strategy: - matrix: - bun-version: [1.0.0] - # See Bun releases at https://github.com/oven-sh/bun/releases - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: '0' - - - name: Use Bun ${{ matrix.bun-version }} - uses: oven-sh/setup-bun@v1 - with: - bun-version: ${{ matrix.bun-version }} - - - run: bun install - - run: bun test src - - run: bun run build - - run: bun run tsc - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 diff --git a/package-lock.json b/package-lock.json index 0715ba772..06bd0d0d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "nanospinner": "^1.1.0", "open": "^8.4.2", "postcss": "^8.4.31", - "prettier-plugin-tailwindcss": "^0.5.6", + "prettier-plugin-tailwindcss": "^0.5.11", "randomstring": "^1.3.0", "react": "^18.2.0", "react-detect-offline": "^2.4.5", @@ -14654,9 +14654,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.6.tgz", - "integrity": "sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.11.tgz", + "integrity": "sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==", "dev": true, "engines": { "node": ">=14.21.3" @@ -14665,13 +14665,13 @@ "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", - "@shufo/prettier-plugin-blade": "*", "@trivago/prettier-plugin-sort-imports": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-style-order": "*", @@ -14687,9 +14687,6 @@ "@shopify/prettier-plugin-liquid": { "optional": true }, - "@shufo/prettier-plugin-blade": { - "optional": true - }, "@trivago/prettier-plugin-sort-imports": { "optional": true }, diff --git a/package.json b/package.json index ec2331f59..f8fdae433 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,16 @@ "watch": "remix watch", "start": "npm run build && node .", "clean": "rimraf -rf build public/build .cache .temp", - "tsc": "tsc", + "typecheck": "tsc", "pub-pre": "npm version prerelease && npm publish --tag next", "pub-exp": "node ./scripts/publish-experimental.js", "prepublishOnly": "npm run clean && npm run build", - "format": "eslint --cache --fix {src,scripts}/**/*.{ts,tsx,js,mjs} && prettier --log-level warn --write {src,scripts}/**/*.{ts,tsx,js,mjs}", - "lint": "eslint --cache --fix {src,scripts}/**/*.{ts,tsx,js,mjs}" + "format:write": "prettier --log-level warn --write src/**/*.{ts,tsx} scripts/**/*.mjs", + "lint:fix": "eslint --fix --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "check:write": "npm run format:write && npm run lint:fix", + "check": "npm run format:check && npm run lint", + "format:check": "prettier --check src/**/*.{ts,tsx} scripts/**/*.mjs", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint ." }, "devDependencies": { "@mdi/js": "^7.3.67", @@ -88,7 +92,7 @@ "nanospinner": "^1.1.0", "open": "^8.4.2", "postcss": "^8.4.31", - "prettier-plugin-tailwindcss": "^0.5.6", + "prettier-plugin-tailwindcss": "^0.5.11", "randomstring": "^1.3.0", "react": "^18.2.0", "react-detect-offline": "^2.4.5", diff --git a/playwright.config.ts b/playwright.config.ts index 610067294..252b66bea 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,11 +1,12 @@ import { defineConfig, devices } from "@playwright/test" +import dotenv from "dotenv"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ -require("dotenv").config() +dotenv.config(); /** * See https://playwright.dev/docs/test-configuration. diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 000000000..a773867ca --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,9 @@ +/** @type {import("prettier").Config} */ +const config = { + semi: false, + printWidth: 120, + trailingComma: "none", + plugins: ["prettier-plugin-tailwindcss"] +} + +export default config diff --git a/src/analyzer/analyze.server.ts b/src/analyzer/analyze.server.ts index a2a26961c..ef78483e8 100644 --- a/src/analyzer/analyze.server.ts +++ b/src/analyzer/analyze.server.ts @@ -127,7 +127,7 @@ async function analyzeTree(path: string, name: string, hash: string) { currTree = currTree.children.find((t) => t.name === treePath && t.type === "tree") as GitTreeObject } switch (child.type) { - case "tree": + case "tree": { const newTree: GitTreeObject = { type: "tree", path: newPath, @@ -139,7 +139,8 @@ async function analyzeTree(path: string, name: string, hash: string) { currTree.children.push(newTree) break - case "blob": + } + case "blob": { fileCount += 1 const blob: GitBlobObject = { type: "blob", @@ -153,6 +154,7 @@ async function analyzeTree(path: string, name: string, hash: string) { // jobs.push((async () => (blob.blameAuthors = await GitCaller.getInstance().parseBlame(blob.path)))()) currTree.children.push(blob) break + } } } @@ -187,7 +189,9 @@ export async function updateTruckConfig(repoDir: string, updaterFn: (tc: TruckUs try { const configFileContents = await fs.readFile(truckConfigPath, "utf-8") if (configFileContents) currentConfig = JSON.parse(configFileContents) - } catch (e) {} + } catch (e) { + // TODO: Show error in UI that the truckconfig.json file could not be read + } const updatedConfig = updaterFn(currentConfig) await fs.writeFile(truckConfigPath, JSON.stringify(updatedConfig, null, 2)) } diff --git a/src/analyzer/hydrate.server.ts b/src/analyzer/hydrate.server.ts index 95e6ea3df..a96719059 100644 --- a/src/analyzer/hydrate.server.ts +++ b/src/analyzer/hydrate.server.ts @@ -90,7 +90,7 @@ async function updateCreditOnBlob(blob: HydratedGitBlobObject, commit: GitLogEnt } return } - if (change.contribs === 0) return // in case a rename with no changes, this happens + if (change.contribs === 0) return // in case a rename with no changes, this happens blob.authors[commit.author] = (blob.authors[commit.author] ?? 0) + change.contribs for (const coauthor of commit.coauthors) { @@ -102,7 +102,7 @@ export let progress = 0 export let totalCommitCount = Infinity export async function hydrateData(commit: GitCommitObject): Promise<[HydratedGitCommitObject, string[]]> { - setFlagsFromString('--max-old-space-size=4096') + setFlagsFromString("--max-old-space-size=4096") const data = commit as HydratedGitCommitObject const fileMap = convertFileTreeToMap(data.tree) initially_mut(data) @@ -144,7 +144,7 @@ export async function hydrateData(commit: GitCommitObject): Promise<[HydratedGit } function sortCommits(fileMap: Map) { - fileMap.forEach((blob, _) => { + fileMap.forEach((blob) => { const cast = blob as HydratedGitBlobObject cast.commits?.sort((a, b) => { return b.time - a.time diff --git a/src/components/Chart.tsx b/src/components/Chart.tsx index f564479ac..a7522529a 100644 --- a/src/components/Chart.tsx +++ b/src/components/Chart.tsx @@ -242,7 +242,7 @@ function collapseText({ } else { const datum = d as HierarchyRectangularNode textIsTooLong = (text: string) => datum.x1 - datum.x0 < text.length * estimatedLetterWidth - textIsTooTall = (text: string) => { + textIsTooTall = () => { const heightAvailable = datum.y1 - datum.y0 - (isBlob(d.data) ? treemapBlobTextOffsetY : treemapTreeTextOffsetY) return heightAvailable < estimatedLetterHeightForDirText } @@ -394,7 +394,7 @@ function createPartitionedHiearchy( }) const cutOff = Number.isNaN(renderCutoff) ? 2 : renderCutoff switch (chartType) { - case "TREE_MAP": + case "TREE_MAP": { const treeMapPartition = treemap() .tile(treemapBinary) .size([size.width, size.height]) @@ -406,12 +406,12 @@ function createPartitionedHiearchy( filterTree(tmPartition, (child) => { const cast = child as HierarchyRectangularNode - return (cast.x1 - cast.x0) >= cutOff && (cast.y1 - cast.y0) >= cutOff + return cast.x1 - cast.x0 >= cutOff && cast.y1 - cast.y0 >= cutOff }) return tmPartition - - case "BUBBLE_CHART": + } + case "BUBBLE_CHART": { const bubbleChartPartition = pack() .size([size.width, size.height - estimatedLetterHeightForDirText]) .padding(bubblePadding) @@ -421,6 +421,7 @@ function createPartitionedHiearchy( return cast.r >= cutOff }) return bPartition + } default: throw Error("Invalid chart type") } diff --git a/src/components/CommitHistory.tsx b/src/components/CommitHistory.tsx index b7769da59..4811d08c3 100644 --- a/src/components/CommitHistory.tsx +++ b/src/components/CommitHistory.tsx @@ -37,11 +37,12 @@ function CommitDistFragment(props: CommitDistFragProps) {
  • (props.handleOnClick ? props.handleOnClick(value) : null)} key={value.hash + "--itemContentAccordion"} title={`By: ${value.author}`} > - {value.message} +
  • ) })} @@ -164,7 +165,9 @@ export function CommitHistory() { commit.message.toLowerCase().includes(commitSearch.toLowerCase())) + ? commits.filter((commit: GitLogEntry) => + commit.message.toLowerCase().includes(commitSearch.toLowerCase()) + ) : commits } sortBy={commitSortingMethodsType} @@ -172,12 +175,12 @@ export function CommitHistory() { {fetcher.state === "idle" ? ( commitIndex + commitIncrement < totalCommitHashes.length ? ( - setCommitIndex(commitIndex + commitIncrement)} className="whitespace-pre text-xs font-medium opacity-70 hover:cursor-pointer" > Load more commits {footerText} - + ) : ( {footerText} ) diff --git a/src/components/FeedbackCard.tsx b/src/components/FeedbackCard.tsx index 24bc5a88a..a1e67af02 100644 --- a/src/components/FeedbackCard.tsx +++ b/src/components/FeedbackCard.tsx @@ -1,9 +1,9 @@ import { mdiForum } from "@mdi/js" import Icon from "@mdi/react" import { memo } from "react" -import GitHubButton from 'react-github-btn' +import GitHubButton from "react-github-btn" -export const FeedbackCard = memo(function FeedbackCard() { +export const FeedbackCard = memo(function FeedbackCard() { return (
    @@ -16,13 +16,17 @@ export const FeedbackCard = memo(function FeedbackCard() { data-icon="octicon-star" data-size="large" data-show-count="true" - >Star Git Truck + > + Star Git Truck + Open an issue + > + Open an issue +
    ) diff --git a/src/components/HiddenFiles.tsx b/src/components/HiddenFiles.tsx index a3b0d6617..19cbb6da3 100644 --- a/src/components/HiddenFiles.tsx +++ b/src/components/HiddenFiles.tsx @@ -21,14 +21,12 @@ export const HiddenFiles = memo(function HiddenFiles() { return (
    -

    setExpanded(!expanded)} - role="button" - > - - Hidden files ({analyzerData.hiddenFiles.length}) - +

    +

    {expanded ? (
    diff --git a/src/components/LoadingIndicator.tsx b/src/components/LoadingIndicator.tsx index b6bb6e54f..988075710 100644 --- a/src/components/LoadingIndicator.tsx +++ b/src/components/LoadingIndicator.tsx @@ -15,7 +15,7 @@ export function LoadingIndicator({ className = "" }: { loadingText?: string; cla useEffect(() => { if (fetcher.state === "idle") fetcher.load(`/progress`) - }, [fetcher.state]) + }, [fetcher, fetcher.state]) const progressText = useMemo(() => { if (!fetcher.data) return "Starting analyzation" diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index b44186508..312c85dc7 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -78,15 +78,17 @@ function ColorMetricDependentInfo(props: { authorshipType: AuthorshipType }) { switch (props.metric) { - case "MOST_COMMITS": + case "MOST_COMMITS": { const noCommits = props.hoveredBlob?.noCommits if (!noCommits) return null return `${noCommits} commit${noCommits > 1 ? "s" : ""}` - case "LAST_CHANGED": + } + case "LAST_CHANGED": { const epoch = props.hoveredBlob?.lastChangeEpoch if (!epoch) return null - return <>{dateFormatRelative(epoch)} - case "SINGLE_AUTHOR": + return dateFormatRelative(epoch) + } + case "SINGLE_AUTHOR": { const authors = props.hoveredBlob ? Object.entries(props.hoveredBlob?.unionedAuthors?.[props.authorshipType] ?? []) : [] @@ -98,11 +100,13 @@ function ColorMetricDependentInfo(props: { default: return `${authors.length} authors` } - case "TOP_CONTRIBUTOR": + } + case "TOP_CONTRIBUTOR": { const dominant = props.hoveredBlob?.dominantAuthor?.[props.authorshipType] ?? undefined if (!dominant) return null - return <>{dominant[0]} - case "TRUCK_FACTOR": + return dominant[0] + } + case "TRUCK_FACTOR": { const authorCount = Object.entries(props.hoveredBlob?.unionedAuthors?.HISTORICAL ?? []).length switch (authorCount) { case 0: @@ -112,6 +116,7 @@ function ColorMetricDependentInfo(props: { default: return `${authorCount} authors` } + } default: return null } diff --git a/src/components/UnionAuthorsModal.tsx b/src/components/UnionAuthorsModal.tsx index 74ebedaca..a9e403ed7 100644 --- a/src/components/UnionAuthorsModal.tsx +++ b/src/components/UnionAuthorsModal.tsx @@ -1,6 +1,4 @@ import { useTransition, useState } from "react" -import type { MouseEvent } from "react" - import { useNavigation, useSubmit } from "@remix-run/react" import { useData } from "~/contexts/DataContext" import { getPathFromRepoAndHead } from "~/util" @@ -83,10 +81,6 @@ export function UnionAuthorsModal({ visible, onClose }: { visible: boolean; onCl .slice(0) .sort(stringSorter) - function handleModalWrapperClick(event: MouseEvent) { - if (event.target === event.currentTarget) onClose() - } - useKey("Escape", onClose) const getColorFromDisplayName = (displayName: string) => authorColors.get(displayName) ?? "#333" @@ -153,7 +147,7 @@ export function UnionAuthorsModal({ visible, onClose }: { visible: boolean; onCl }) return ( -
    +

    Group authors

    diff --git a/src/components/accordion/AccordionItem.tsx b/src/components/accordion/AccordionItem.tsx index f60a38226..4478e814c 100644 --- a/src/components/accordion/AccordionItem.tsx +++ b/src/components/accordion/AccordionItem.tsx @@ -17,8 +17,8 @@ function AccordionItem({ return (
  • -
    {data.title} -
    +

      { + setStartDate: () => { throw new Error("No setStartDate provided") }, - setEndDate: (newDate: number | null) => { + setEndDate: () => { throw new Error("No setEndDate provided") } } diff --git a/src/metrics/singleAuthor.ts b/src/metrics/singleAuthor.ts index 746e58aeb..5b472d51d 100644 --- a/src/metrics/singleAuthor.ts +++ b/src/metrics/singleAuthor.ts @@ -3,26 +3,34 @@ import type { PointLegendData } from "~/components/legend/PointLegend" import { PointInfo } from "~/components/legend/PointLegend" import type { AuthorshipType, MetricCache } from "./metrics" -export function setDominanceColor(blob: HydratedGitBlobObject, cache: MetricCache, authorshipType: AuthorshipType, authorColors: Map) { +export function setDominanceColor( + blob: HydratedGitBlobObject, + cache: MetricCache, + authorshipType: AuthorshipType, + authorColors: Map +) { const multipleAuthorsColor = "#e0e0e0" const noAuthorsColor = "#404040" - + const authorUnion = blob.unionedAuthors?.[authorshipType] ?? {} - + const legend = cache.legend as PointLegendData const authors = Object.keys(authorUnion) switch (authors.length) { - case 0: + case 0: { legend.set("No authors", new PointInfo(noAuthorsColor, 0)) cache.colormap.set(blob.path, noAuthorsColor) return - case 1: + } + case 1: { const color = authorColors.get(authors[0]) ?? noAuthorsColor legend.set(authors[0], new PointInfo(color, 2)) cache.colormap.set(blob.path, color) return - default: + } + default: { legend.set("Multiple authors", new PointInfo(multipleAuthorsColor, 1)) cache.colormap.set(blob.path, multipleAuthorsColor) + } } } diff --git a/src/root.tsx b/src/root.tsx index 03a4e24c4..82343d794 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -72,9 +72,9 @@ export const ErrorBoundary = () => { if (isRouteErrorResponse(error)) { return ( - + - Oops! An error wasn't handled + Oops! An error wasn't handled @@ -87,9 +87,9 @@ export const ErrorBoundary = () => { ) } else if (error instanceof Error) { return ( - + - Oops! An error wasn't handled + Oops! An error wasn't handled diff --git a/src/routes/$repo.$.tsx b/src/routes/$repo.$.tsx index 677ede601..4e2829670 100644 --- a/src/routes/$repo.$.tsx +++ b/src/routes/$repo.$.tsx @@ -33,7 +33,7 @@ import clsx from "clsx" import { Tooltip } from "~/components/Tooltip" import { createPortal } from "react-dom" import randomstring from "randomstring" -import { Online } from "react-detect-offline"; +import { Online } from "react-detect-offline" let invalidateCache = false @@ -267,7 +267,7 @@ export default function Repo() { > {!isFullscreen ? (
      -
      setIsLeftPanelCollapse(!isLeftPanelCollapse)} className={clsx( "absolute top-half-screen flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-2 border-solid border-sky-500 bg-white", @@ -277,7 +277,7 @@ export default function Repo() { )} > -
      +
      ) : null} {!isLeftPanelCollapse ? ( @@ -305,12 +305,12 @@ export default function Repo() { > {!isFullscreen ? (
      -
      setIsRightPanelCollapse(!isRightPanelCollapse)} className="absolute right-0 top-half-screen flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-2 border-solid border-sky-500 bg-white" > -
      +
      ) : null} {!isRightPanelCollapse ? ( From 866669e94b32ecbe627697fe4e3f3d2bce57d0a0 Mon Sep 17 00:00:00 2001 From: Automated Version Bump Date: Wed, 21 Feb 2024 12:47:31 +0000 Subject: [PATCH 2/6] Bump version to v1.9.1 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06bd0d0d4..0b15a2c67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "git-truck", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "git-truck", - "version": "1.9.0", + "version": "1.9.1", "license": "MIT", "bin": { "git-truck": "cli.js" diff --git a/package.json b/package.json index f8fdae433..97c214075 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-truck", - "version": "1.9.0", + "version": "1.9.1", "private": false, "description": "Visualizing a Git repository", "license": "MIT", From d774e1ea62a049e19104c62d306029ad1af05977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20R=C3=B8ssum?= <1959615+joglr@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:03:59 +0100 Subject: [PATCH 3/6] Add dark mode (#725) Fixes #723 This PR adds dark mode. The user preference is automatically applied initially, and can be overridden as a setting. In the future, we will clean up the UI, but for now it lives with the rest of the settings. --- .github/workflows/check-bun.yml | 10 +- .vscode/extensions.json | 3 +- .vscode/settings.json | 5 +- package-lock.json | 36 ++-- package.json | 3 +- src/components/Chart.tsx | 27 ++- src/components/CommitsCard.tsx | 4 +- src/components/DetailsCard.tsx | 59 +++---- src/components/EnumSelect.tsx | 2 +- src/components/FeedbackCard.tsx | 36 ++-- src/components/LoadingIndicator.tsx | 7 +- src/components/MenuTab.tsx | 5 +- src/components/Options.tsx | 33 +++- src/components/Providers.tsx | 2 +- src/components/RevisionSelect.tsx | 2 +- src/components/Tooltip.tsx | 2 +- src/components/UnionAuthorsModal.tsx | 246 +++++++++++++++++---------- src/components/util.tsx | 2 +- src/const.ts | 1 - src/contexts/OptionsContext.ts | 5 +- src/hooks.ts | 12 ++ src/root.tsx | 30 +++- src/routes/$repo.$.tsx | 84 +++++---- src/routes/_index.tsx | 101 +++++------ src/styles/animations.css | 41 +++++ src/styles/index.css | 79 --------- src/styles/vars.css | 13 -- src/styling.tsx | 44 +++++ src/tailwind.css | 71 ++++++-- tailwind.config.js | 15 +- 30 files changed, 570 insertions(+), 410 deletions(-) create mode 100644 src/styles/animations.css delete mode 100644 src/styles/index.css create mode 100644 src/styling.tsx diff --git a/.github/workflows/check-bun.yml b/.github/workflows/check-bun.yml index 707691b89..2b96164fe 100644 --- a/.github/workflows/check-bun.yml +++ b/.github/workflows/check-bun.yml @@ -3,10 +3,12 @@ name: Check (bun) on: - push: - branches: [main] - pull_request: - branches: [main] + workflow_dispatch: + + # push: + # branches: [main] + # pull_request: + # branches: [main] jobs: check-bun: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 17883dd86..813afaf04 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss", "Orta.vscode-jest", - "ms-playwright.playwright" + "ms-playwright.playwright", + "dbaeumer.vscode-eslint" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 36f1fb126..64a880580 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "typescript.experimental.tsserver.web.enableProjectWideIntellisense": false, "files.autoSave": "off", - "css.lint.unknownAtRules": "ignore" + "css.lint.unknownAtRules": "ignore", + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/package-lock.json b/package-lock.json index 0b15a2c67..4ff17d04e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,8 @@ "remix-typedjson": "^0.3.1", "rimraf": "^5.0.5", "semver": "^7.5.4", - "tailwindcss": "^3.3.5", + "tailwind-merge": "^2.2.1", + "tailwindcss": "^3.4.1", "tiny-invariant": "^1.3.1", "ts-jest": "^29.1.1", "tsup": "^7.2.0", @@ -854,11 +855,12 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dev": true, - "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -15260,9 +15262,10 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "dev": true, - "license": "MIT" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", @@ -16525,10 +16528,23 @@ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", "dev": true }, + "node_modules/tailwind-merge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz", + "integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", - "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", diff --git a/package.json b/package.json index 97c214075..f40c425d6 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,8 @@ "remix-typedjson": "^0.3.1", "rimraf": "^5.0.5", "semver": "^7.5.4", - "tailwindcss": "^3.3.5", + "tailwind-merge": "^2.2.1", + "tailwindcss": "^3.4.1", "tiny-invariant": "^1.3.1", "ts-jest": "^29.1.1", "tsup": "^7.2.0", diff --git a/src/components/Chart.tsx b/src/components/Chart.tsx index a7522529a..616e44fd5 100644 --- a/src/components/Chart.tsx +++ b/src/components/Chart.tsx @@ -14,7 +14,6 @@ import { bubblePadding, estimatedLetterHeightForDirText, estimatedLetterWidth, - searchMatchColor, circleTreeTextOffsetY, treemapBlobTextOffsetX, treemapBlobTextOffsetY, @@ -33,6 +32,7 @@ import { getTextColorFromBackground, isBlob, isTree } from "~/util" import clsx from "clsx" import type { SizeMetricType } from "~/metrics/sizeMetric" import { useSearch } from "~/contexts/SearchContext" +import { cn, usePrefersLightMode } from "~/styling" type CircleOrRectHiearchyNode = HierarchyCircularNode | HierarchyRectangularNode @@ -125,7 +125,7 @@ export const Chart = memo(function Chart({
      { let props: JSX.IntrinsicElements["rect"] = { - stroke: isSearchMatch ? searchMatchColor : "transparent", strokeWidth: "1px", fill: isBlob(d.data) ? metricsData[authorshipType].get(metricType)?.colormap.get(d.data.path) ?? "grey" @@ -205,16 +204,15 @@ function Node({ d, isSearchMatch }: { d: CircleOrRectHiearchyNode; isSearchMatch } } return props - }, [d, isSearchMatch, metricsData, authorshipType, metricType, chartType]) + }, [d, metricsData, authorshipType, metricType, chartType]) return ( ) @@ -285,6 +283,7 @@ function collapseText({ function NodeText({ d, children = null }: { d: CircleOrRectHiearchyNode; children?: React.ReactNode }) { const [metricsData] = useMetrics() const { authorshipType, metricType } = useOptions() + const prefersLightMode = usePrefersLightMode() const isBubbleChart = isCircularNode(d) if (children === null) return null @@ -308,7 +307,9 @@ function NodeText({ d, children = null }: { d: CircleOrRectHiearchyNode; childre const fillColor = isBlob(d.data) ? getTextColorFromBackground(metricsData[authorshipType].get(metricType)?.colormap.get(d.data.path) ?? "#333") - : "#333" + : prefersLightMode + ? "#333" + : "#fff" const textPathBaseProps = { startOffset: isBubbleChart ? "50%" : undefined, @@ -320,20 +321,18 @@ function NodeText({ d, children = null }: { d: CircleOrRectHiearchyNode; childre return ( <> - {isTree(d.data) ? ( + {isTree(d.data) && isBubbleChart ? ( {children} ) : null} - + -
      +
      -
      +
      diff --git a/src/components/DetailsCard.tsx b/src/components/DetailsCard.tsx index 2f8887703..f47be0703 100644 --- a/src/components/DetailsCard.tsx +++ b/src/components/DetailsCard.tsx @@ -17,6 +17,7 @@ import clsx from "clsx" import { useMetrics } from "~/contexts/MetricContext" import { MenuItem, MenuTab } from "./MenuTab" import { CommitsCard } from "./CommitsCard" +import { usePrefersLightMode } from "~/styling" function OneFolderOut(path: string) { const index = path.lastIndexOf("/") @@ -40,6 +41,7 @@ export function DetailsCard({ const { setPath, path } = usePath() const { analyzerData } = useData() const isProcessingHideRef = useRef(false) + const prefersLightMode = usePrefersLightMode() useEffect(() => { if (isProcessingHideRef.current) { @@ -54,23 +56,23 @@ export function DetailsCard({ }, [analyzerData, setClickedObject]) const [metricsData] = useMetrics() - const { backgroundColor, color, lightBackground } = useMemo(() => { + const { backgroundColor, lightBackground } = useMemo(() => { if (!clickedObject) { return { backgroundColor: null, - color: null, - lightBackground: true + color: null } } const colormap = metricsData[authorshipType]?.get(metricType)?.colormap - const backgroundColor = colormap?.get(clickedObject.path) ?? ("#808080" as `#${string}`) + const backgroundColor = + colormap?.get(clickedObject.path) ?? ((prefersLightMode ? "#808080" : "#262626") as `#${string}`) const color = backgroundColor ? getTextColorFromBackground(backgroundColor) : null return { backgroundColor: backgroundColor, color: color, lightBackground: color === "#000000" } - }, [clickedObject, metricsData, metricType, authorshipType]) + }, [clickedObject, metricsData, metricType, authorshipType, prefersLightMode]) if (!clickedObject) return null const isBlob = clickedObject.type === "blob" @@ -78,18 +80,20 @@ export function DetailsCard({ return (
      -

      +

      {clickedObject.name} @@ -97,7 +101,7 @@ export function DetailsCard({ setClickedObject(null)} />

      - +
      @@ -112,20 +116,14 @@ export function DetailsCard({ )}
      -
      +
      {isBlob ? ( ) : ( )}
      - @@ -136,10 +134,7 @@ export function DetailsCard({
      ))} diff --git a/src/components/FeedbackCard.tsx b/src/components/FeedbackCard.tsx index a1e67af02..2dee1c852 100644 --- a/src/components/FeedbackCard.tsx +++ b/src/components/FeedbackCard.tsx @@ -10,23 +10,25 @@ export const FeedbackCard = memo(function FeedbackCard() {

      Support Git Truck

      -
      - - Star Git Truck - - - Open an issue - +
      +
      + + Star Git Truck + + + Open an issue + +
      ) diff --git a/src/components/LoadingIndicator.tsx b/src/components/LoadingIndicator.tsx index 988075710..11f13c7ac 100644 --- a/src/components/LoadingIndicator.tsx +++ b/src/components/LoadingIndicator.tsx @@ -27,12 +27,7 @@ export function LoadingIndicator({ className = "" }: { loadingText?: string; cla }, [fetcher.data]) return ( -
      +

      {progressText}

      {"🚛"} diff --git a/src/components/MenuTab.tsx b/src/components/MenuTab.tsx index e816719ed..4befc4a70 100644 --- a/src/components/MenuTab.tsx +++ b/src/components/MenuTab.tsx @@ -15,7 +15,6 @@ export type MenuItemProps = { } export type MenuTabProps = { - lightBackground?: boolean onChange?: (index: number) => void selectedItemIndex?: number } @@ -44,9 +43,7 @@ export const MenuTab = (props: PropsWithChildren) => { {items.map((item, idx) => ( @@ -146,78 +185,101 @@ export function UnionAuthorsModal({ visible, onClose }: { visible: boolean; onCl ) }) - return ( -
      -
      -

      Group authors

      - -

      Ungrouped authors

      - - {ungroupedAuthorsSorted.length > 0 ? ( -
      - startTransition(() => setFilter(e.target.value))} - /> - - -
      - ) : ( -
      - )} -

      {ungroupedAuthorsMessage}

      - -
      {ungroupedAuthersEntries}
      + return createPortal( + +
      +

      Group authors

      + -
      +

      Ungrouped authors ({ungroupedAuthorsSorted.length})

      +

      Grouped authors

      -

      Grouped authors

      -
      - {authorUnions.length > 0 ? ( - - ) : ( -
      - )} +
      + +
      +
      +
      -

      {groupedAuthorsMessage}

      -
      - {authorUnions.length > 0 ? groupedAuthorsEntries : null} +
      +
      +
      + startTransition(() => setFilter(e.target.value))} + /> + + +
      + {ungroupedAuthorsEntries.length > 0 ? ( + ungroupedAuthorsEntries + ) : ( +

      + {filter.length > 0 ? "No authors found" : "All authors have been grouped"} +

      + )} +
      +
      +
      +
      + {authorUnions.length > 0 ? ( + groupedAuthorsEntries + ) : ( +

      No authors have been grouped yet

      + )} +
      -
      -
      - -
      -
      +
      , + document.body ) function AliasEntry({ @@ -238,7 +300,9 @@ export function UnionAuthorsModal({ visible, onClose }: { visible: boolean; onCl title="Make display name for this grouping" > - + ) } diff --git a/src/components/util.tsx b/src/components/util.tsx index 7ad4bc1ab..ba28918dd 100644 --- a/src/components/util.tsx +++ b/src/components/util.tsx @@ -33,7 +33,7 @@ export const LegendDot = ({ export const Code = ({ inline = false, ...props }: { inline?: boolean } & HTMLAttributes) => ( { + const mediaQuery = window.matchMedia(query) + setMatches(mediaQuery.matches) + const listener = () => setMatches(mediaQuery.matches) + mediaQuery.addEventListener("change", listener) + return () => mediaQuery.removeEventListener("change", listener) + }, [query]) + return matches +} diff --git a/src/root.tsx b/src/root.tsx index 82343d794..96d06668b 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -12,9 +12,10 @@ import { } from "@remix-run/react" import varsStyles from "~/styles/vars.css" -import indexStyles from "~/styles/index.css" +import indexStyles from "~/styles/animations.css" import { Code } from "./components/util" import tailwindStylesheet from "~/tailwind.css" +import { ThemeProvider, cn, usePrefersLightMode } from "./styling" export const meta: MetaFunction = () => { return [{ title: "Git Truck" }] @@ -57,16 +58,31 @@ export default function App() { - - - - - - + + + + + + + + ) } +function Body({ children }: { children: React.ReactNode }) { + const prefersLightMode = usePrefersLightMode() + return ( + + {children} + + ) +} + export const ErrorBoundary = () => { const error = useRouteError() diff --git a/src/routes/$repo.$.tsx b/src/routes/$repo.$.tsx index 4e2829670..5f0058eff 100644 --- a/src/routes/$repo.$.tsx +++ b/src/routes/$repo.$.tsx @@ -1,6 +1,6 @@ import { resolve } from "path" import type { Dispatch, SetStateAction } from "react" -import { memo, useEffect, useRef, useState } from "react" +import { memo, useEffect, useMemo, useRef, useState } from "react" import { useBoolean, useMouse } from "react-use" import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node" import { redirect } from "@remix-run/node" @@ -34,6 +34,7 @@ import { Tooltip } from "~/components/Tooltip" import { createPortal } from "react-dom" import randomstring from "randomstring" import { Online } from "react-detect-offline" +import { cn } from "~/styling" let invalidateCache = false @@ -236,41 +237,61 @@ export default function Repo() { const [hoveredObject, setHoveredObject] = useState(null) const showUnionAuthorsModal = (): void => setUnionAuthorsModalOpen(true) - if (!analyzerData) return null + const containerClass = useMemo( + function getContainerClass() { + // The fullscreen overrides the collapses + if (isFullscreen) { + return "fullscreen" + } - function defineTheContainerClass(): string { - // The fullscreen overrides the collapses - if (isFullscreen) { - return "fullscreen" - } + // The classes for collapses + if (isLeftPanelCollapse && isRightPanelCollapse) { + return "both-collapse" + } - // The classes for collapses - if (isLeftPanelCollapse && isRightPanelCollapse) { - return "both-collapse" - } else if (isLeftPanelCollapse) { - return "left-collapse" - } else if (isRightPanelCollapse) { - return "right-collapse" - } + if (isLeftPanelCollapse) { + return "left-collapse" + } - // The default class is none - return "" - } + if (isRightPanelCollapse) { + return "right-collapse" + } + + // The default class is none + return "" + }, + [isFullscreen, isLeftPanelCollapse, isRightPanelCollapse] + ) + + if (!analyzerData) return null return ( -
      +
      @@ -299,15 +312,16 @@ export default function Repo() {
      ) return ( -
      +

      Git Truck @@ -76,70 +78,57 @@ function RepositoryGrid({ repositories }: { repositories: SerializeFrom ) : ( - <> - - + +
      + {repositories.map((repo, idx) => ( + + ))} +

      ) } -function RepositoryEntry({ repo }: { repo: SerializeFrom }): JSX.Element { +function RepositoryEntry({ repo }: { repo: SerializeFrom; index: number }): ReactNode { const [head, setHead] = useState(repo.currentHead) const path = getPathFromRepoAndHead(repo.name, head) const branchIsAnalyzed = repo.analyzedHeads[head] return ( -
      -
      -

      -

      {repo.name}

      - - {branchIsAnalyzed ? "Ready" : "Not analyzed"} - -
  • -
    - setHead(e.target.value)} - headGroups={repo.refs} - analyzedHeads={repo.analyzedHeads} - /> -
    - - {branchIsAnalyzed ? "View" : "Analyze"} - -
    -
    + +

    + {repo.name} +

    + + {branchIsAnalyzed ? "Ready" : "Not analyzed"} + + setHead(e.target.value)} + headGroups={repo.refs} + analyzedHeads={repo.analyzedHeads} + /> +
    + + {branchIsAnalyzed ? "View" : "Analyze"} +
    -
    + ) } diff --git a/src/styles/animations.css b/src/styles/animations.css new file mode 100644 index 000000000..c52c56533 --- /dev/null +++ b/src/styles/animations.css @@ -0,0 +1,41 @@ +@keyframes dash { + to { + stroke-dashoffset: 0; + } +} + +@keyframes hide_initially { + to { + opacity: 1; + } +} + +@keyframes blink { + 0% { + opacity: 100%; + } + 50% { + opacity: 50%; + } + 100% { + opacity: 100%; + } +} + +@keyframes stroke_pulse { + 0% { + stroke-width: 1px; + stroke-opacity: 0%; + } + 50% { + stroke-opacity: 50%; + } + 99% { + stroke-width: 5px; + stroke-opacity: 0%; + } + 100% { + stroke-width: 1px; + stroke-opacity: 0%; + } +} diff --git a/src/styles/index.css b/src/styles/index.css deleted file mode 100644 index c96ccac7f..000000000 --- a/src/styles/index.css +++ /dev/null @@ -1,79 +0,0 @@ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - margin: 0; - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", - "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - min-height: 100vh; - - background-color: var(--global-bg-color); -} - -#root { - height: 100%; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; -} - -input, -button, -textarea, -select { - font: inherit; -} - -button:focus, -input:focus, -select:focus { - outline-color: var(--focused-outline-color); -} - -@keyframes dash { - to { - stroke-dashoffset: 0; - } -} - -@keyframes hide_initially { - to { - opacity: 1; - } -} - -@keyframes blink { - 0% { - opacity: 100%; - } - 50% { - opacity: 50%; - } - 100% { - opacity: 100%; - } -} - -@keyframes stroke_pulse { - 0% { - stroke-width: 1px; - stroke-opacity: 0%; - } - 50% { - stroke-opacity: 50%; - } - 99% { - stroke-width: 5px; - stroke-opacity: 0%; - } - 100% { - stroke-width: 1px; - stroke-opacity: 0%; - } -} diff --git a/src/styles/vars.css b/src/styles/vars.css index e10781490..dc6dbea94 100644 --- a/src/styles/vars.css +++ b/src/styles/vars.css @@ -1,22 +1,9 @@ :root { --unit: 4px; - --global-bg-color: hsl(210, 38%, 95%); --side-panel-width-units: 94; --side-panel-width: calc(var(--side-panel-width-units) * var(--unit)); - --title-color: hsl(200, 5%, 30%); - --text-color: hsl(200, 5%, 25%); - --border-color: hsl(0, 0%, 70%); - --border-width: 1px; - --surface-color: hsl(0, 0%, 90%); - --border-color-alpha: hsla(0, 0%, 0%, 30%); - --focused-outline-color: hsl(216, 83%, 62%); /* Generated with: https://shadows.brumm.af/ */ --shadow: 0.9px 0.9px 2.7px hsla(0, 0%, 0%, 0.07), 2.2px 2.2px 6.9px hsla(0, 0%, 0%, 0.048), 4.4px 4.4px 14.2px hsla(0, 0%, 0%, 0.039), 9.1px 9.1px 29.2px hsla(0, 0%, 0%, 0.031), 25px 25px 80px hsla(0, 0%, 0%, 0.022); - --small-shadow: 0.9px 0.9px 1.7px hsla(0, 0%, 0%, 0.5); - - --hover-transition-duration: 200ms; - --button-text-color: hsl(216, 0%, 30%); } - \ No newline at end of file diff --git a/src/styling.tsx b/src/styling.tsx new file mode 100644 index 000000000..5a1891c70 --- /dev/null +++ b/src/styling.tsx @@ -0,0 +1,44 @@ +import { type ClassValue, clsx } from "clsx" +import type { Dispatch, ReactNode, SetStateAction } from "react" +import { createContext, useContext } from "react" +import { useLocalStorage } from "react-use" +import { twMerge } from "tailwind-merge" +import { useMediaQuery } from "./hooks" + +export const cn = (...args: ClassValue[]) => twMerge(clsx(args)) + +export const Themes = { + SYSTEM: "System", + LIGHT: "Light", + DARK: "Dark" +} as const + +export type Theme = keyof typeof Themes + +export const ThemeContext = createContext<[Theme, Dispatch>]>([ + "SYSTEM", + () => { + throw new Error("ThemeContext not in the tree") + } +]) + +export const useTheme = () => useContext(ThemeContext) + +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const [theme, setTheme] = useLocalStorage("themeType", undefined) + + return ( + {children} + ) +} + +function useMQPrefersColorSchemeLight() { + return useMediaQuery("(prefers-color-scheme: light)") +} + +export function usePrefersLightMode() { + const prefersLight = useMQPrefersColorSchemeLight() + const [theme] = useTheme() + + return theme === "SYSTEM" || theme === undefined ? prefersLight : theme === "LIGHT" +} diff --git a/src/tailwind.css b/src/tailwind.css index 6fdc11ab1..18892e425 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -2,9 +2,51 @@ @tailwind components; @tailwind utilities; +@layer base { + * { + @apply border-gray-300 dark:border-gray-700; + } + + body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + input, + button, + textarea, + select { + font: inherit; + color: inherit; + background-color: inherit; + } + + input, + button, + textarea, + select { + @apply outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white; + } + + select { + color: inherit; + background-color: inherit; + } + + optgroup, + option { + color: initial; + background-color: initial; + } + + code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + } +} + @layer components { .card { - @apply relative flex w-auto flex-col gap-2 rounded-lg bg-white p-2 shadow-md; + @apply relative flex w-auto flex-col gap-2 rounded-lg bg-gray-100 p-2 shadow-md dark:bg-gray-800; } .card__header { @@ -19,7 +61,7 @@ @apply text-sm opacity-80; } .card__title { - @apply select-none text-base-styles text-sm uppercase tracking-widest font-bold opacity-80; + @apply text-base-styles select-none text-sm font-bold uppercase tracking-widest opacity-80; } .card__subtitle { @@ -29,7 +71,7 @@ /* Inputs */ .btn { - @apply select-none border grid h-8 cursor-pointer grid-flow-col items-center justify-center gap-2 rounded-md bg-gray-200 px-2 text-sm font-bold text-gray-600 transition-all hover:opacity-70 disabled:cursor-not-allowed disabled:opacity-50; + @apply grid h-8 cursor-pointer select-none grid-flow-col items-center justify-center gap-2 text-nowrap rounded-md border border-gray-200 bg-gray-200 fill-current px-2 text-sm font-bold text-gray-600 transition-all hover:opacity-70 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-600 dark:text-gray-200; } .btn > svg, @@ -42,7 +84,7 @@ } .btn--outlined { - @apply border-current bg-transparent text-black/90 hover:bg-transparent hover:text-black/50; + @apply border-current bg-transparent text-inherit hover:bg-transparent hover:opacity-50 dark:hover:opacity-50; } .btn--outlined--light { @@ -50,23 +92,23 @@ } .btn.btn--primary { - @apply bg-blue-500 border-blue-500 text-white enabled:hover:bg-blue-600; + @apply border-blue-500 bg-blue-500 text-white enabled:hover:opacity-70 dark:border-blue-700 dark:bg-blue-700; } .btn--icon.btn--primary { - @apply text-blue-500; + @apply text-blue-500 dark:text-blue-600; } .btn--success { - @apply bg-green-500 text-white; + @apply border-green-500 bg-green-500 text-white; } .btn--danger { - @apply bg-red-500 text-white; + @apply border-red-500 bg-red-500 text-white; } .btn--icon { - @apply inline-grid cursor-pointer place-items-center; + @apply inline-grid cursor-pointer place-items-center fill-current; } .btn--icon > * { @@ -93,7 +135,12 @@ .input { @apply h-8 w-full flex-grow; @apply overflow-scroll text-ellipsis rounded border border-gray-300 px-1 text-sm transition-colors; - @apply enabled:cursor-pointer enabled:hover:border-gray-400 disabled:cursor-not-allowed; + @apply enabled:cursor-pointer enabled:hover:border-gray-400 disabled:cursor-not-allowed disabled:opacity-50; + @apply outline-none focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white; + } + + .input--hover-border { + @apply border-transparent hover:border-gray-400; } .input:not(select) { @@ -101,7 +148,7 @@ } .label { - @apply select-none cursor-pointer font-bold hover:opacity-70; + @apply cursor-pointer select-none font-bold hover:opacity-70; } .app-container { @@ -153,7 +200,7 @@ /* Span all available columns */ grid-column: 1 / -1; } - + .left-arrow-space { left: var(--side-panel-width); } diff --git a/tailwind.config.js b/tailwind.config.js index fb24184b1..d29c8ae33 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,23 +1,22 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{tsx,html}"], + darkMode: "selector", theme: { extend: { animation: { "hide-initially": "0s 1s linear forwards hide_initially", blink: "1s linear infinite blink", - "stroke-pulse": "1s ease-in-out infinite stroke_pulse", - }, - inset: { - "half-screen": "50vh", + "stroke-pulse": "1s ease-in-out infinite stroke_pulse" }, fontFamily: { - mono: ["Roboto Mono", "monospace"], + mono: ["Roboto Mono", "monospace"] }, backgroundImage: { - "arrow": "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\")" + arrow: + "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\")" } - }, + } }, - plugins: [], + plugins: [] } From 5b05e9bfa97a7a015b0605b029e2e3c0cdb77176 Mon Sep 17 00:00:00 2001 From: Automated Version Bump Date: Mon, 18 Mar 2024 16:04:17 +0000 Subject: [PATCH 4/6] Bump version to v1.10.0 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ff17d04e..e06ac8857 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "git-truck", - "version": "1.9.1", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "git-truck", - "version": "1.9.1", + "version": "1.10.0", "license": "MIT", "bin": { "git-truck": "cli.js" diff --git a/package.json b/package.json index f40c425d6..cb82480c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-truck", - "version": "1.9.1", + "version": "1.10.0", "private": false, "description": "Visualizing a Git repository", "license": "MIT", From 3f305ede35d5c068b58b13574c42e8340ac7f402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20R=C3=B8ssum?= <1959615+joglr@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:45:49 +0100 Subject: [PATCH 5/6] Fix crash in Safari (#737) This pull request fixes the crash issue in Safari that was reported in issue #736. The crash was caused by a problem with persisting options to local storage. The fix ensures that the options are properly persisted, preventing the crash from occurring. --- src/components/Providers.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 70951b0a4..0cbbf5e8d 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -138,10 +138,14 @@ export function Providers({ children, data }: ProvidersProps) { let canceled = false // Persist options to local storage if (options) { - requestIdleCallback(() => { - if (canceled) return + if (typeof requestIdleCallback === "function") { + requestIdleCallback(() => { + if (canceled) return + localStorage.setItem(OPTIONS_LOCAL_STORAGE_KEY, JSON.stringify(options)) + }) + } else { localStorage.setItem(OPTIONS_LOCAL_STORAGE_KEY, JSON.stringify(options)) - }) + } } return () => { canceled = true From 2b829217e57be3ee34150fe97d99d63a4e0c4d86 Mon Sep 17 00:00:00 2001 From: Automated Version Bump Date: Tue, 19 Mar 2024 15:46:05 +0000 Subject: [PATCH 6/6] Bump version to v1.10.1 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e06ac8857..57a543989 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "git-truck", - "version": "1.10.0", + "version": "1.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "git-truck", - "version": "1.10.0", + "version": "1.10.1", "license": "MIT", "bin": { "git-truck": "cli.js" diff --git a/package.json b/package.json index cb82480c1..b8fd080e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-truck", - "version": "1.10.0", + "version": "1.10.1", "private": false, "description": "Visualizing a Git repository", "license": "MIT",