git-truck 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/build/client/assets/GitTruckInfo-DSTHQLpb.js +2 -0
  2. package/build/client/assets/RepoTabs-CAVKlzaP.js +1 -0
  3. package/build/client/assets/browse-BPS_2Q2o.js +1 -0
  4. package/build/client/assets/browse-DtF-O2nh.js +9 -0
  5. package/build/client/assets/clear-cache-C8-c8bzY.js +1 -0
  6. package/build/client/assets/clear-cache-DczfzTH1.js +1 -0
  7. package/build/client/assets/manifest-708752f7.js +1 -0
  8. package/build/client/assets/{root-CGqKHKfX.js → root-BBgpPS0e.js} +2 -2
  9. package/build/client/assets/root-BDi1Y-Zk.css +1 -0
  10. package/build/client/assets/ui-mrRo7bBy.js +1 -0
  11. package/build/client/assets/view-tqZGjwcM.js +1 -0
  12. package/build/client/assets/view.commits-CsXn1fsk.js +1 -0
  13. package/build/client/assets/view.details-DurbDplo.js +1 -0
  14. package/build/server/assets/{server-build-voprGiDU.js → server-build-v6Kp-efC.js} +440 -126
  15. package/build/server/index.js +1 -1
  16. package/cli.mjs +99 -131
  17. package/package.json +4 -3
  18. package/build/client/assets/RepoTabs-36qUc-RT.js +0 -1
  19. package/build/client/assets/browse-B9wBPRaK.js +0 -9
  20. package/build/client/assets/browse-Bx9O5Mvl.js +0 -1
  21. package/build/client/assets/clear-cache-BJQQzFka.js +0 -1
  22. package/build/client/assets/clear-cache-DkN7P69Z.js +0 -1
  23. package/build/client/assets/dist-Bpk5WHYg.js +0 -2
  24. package/build/client/assets/manifest-601d0239.js +0 -1
  25. package/build/client/assets/root-BK0u4vgl.css +0 -1
  26. package/build/client/assets/ui-Bw635ygf.js +0 -1
  27. package/build/client/assets/view-mmd0Q4B8.js +0 -1
  28. package/build/client/assets/view.commits-b-k-A7BT.js +0 -1
  29. package/build/client/assets/view.details-B7LCPu46.js +0 -1
@@ -6,7 +6,7 @@ import c from "ansi-colors";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import * as React from "react";
8
8
  import { Activity, Fragment as Fragment$1, Suspense, createContext as createContext$1, createElement, createRef, memo, startTransition, use, useCallback, useContext, useDeferredValue, useEffect, useId, useMemo, useReducer, useRef, useState, useSyncExternalStore, useTransition } from "react";
9
- import { mdiAccount, mdiAccountGroup, mdiAccountMultiple, mdiAccountMultipleMinus, mdiAccountMultiplePlus, mdiArrowLeft, mdiArrowRight, mdiArrowUp, mdiArrowUpBoldCircleOutline, mdiChartBubble, mdiChartTree, mdiCheckboxBlank, mdiCheckboxBlankOutline, mdiCheckboxMarked, mdiCheckboxOutline, mdiChevronDown, mdiChevronRight, mdiCircle, mdiCircleSmall, mdiClockEdit, mdiClose, mdiCog, mdiContentCut, mdiDeleteForever, mdiDiceMultipleOutline, mdiEye, mdiEyeOff, mdiEyeOffOutline, mdiFile, mdiFileCodeOutline, mdiFileOutline, mdiFileTree, mdiFilter, mdiFlaskOutline, mdiFolder, mdiFullscreen, mdiFullscreenExit, mdiHelpCircle, mdiInformationOutline, mdiKnife, mdiLabel, mdiLink, mdiMagnify, mdiMagnifyMinus, mdiMenu, mdiOpenInNew, mdiPlus, mdiPlusMinusVariant, mdiPodiumGold, mdiPulse, mdiRefresh, mdiResize, mdiRestore, mdiScaleBalance, mdiSort, mdiSortAscending, mdiSortDescending, mdiSourceBranch, mdiSourceCommit, mdiSourceRepository, mdiTransition, mdiUndo } from "@mdi/js";
9
+ import { mdiAccount, mdiAccountGroup, mdiAccountMultiple, mdiAccountMultipleMinus, mdiAccountMultiplePlus, mdiArrowLeft, mdiArrowRight, mdiArrowUp, mdiArrowUpBoldCircleOutline, mdiChartBubble, mdiChartTree, mdiCheckboxBlank, mdiCheckboxBlankOutline, mdiCheckboxIntermediate, mdiCheckboxMarked, mdiChevronDown, mdiChevronRight, mdiCircle, mdiCircleSmall, mdiClockEdit, mdiClose, mdiCog, mdiContentCut, mdiDeleteForever, mdiDiceMultipleOutline, mdiEye, mdiEyeOff, mdiEyeOffOutline, mdiFile, mdiFileCodeOutline, mdiFileOutline, mdiFileTree, mdiFilter, mdiFlaskOutline, mdiFolder, mdiFullscreen, mdiFullscreenExit, mdiHelpCircle, mdiInformationOutline, mdiKnife, mdiLabel, mdiLink, mdiMagnify, mdiMagnifyMinus, mdiMenu, mdiOpenInNew, mdiPlus, mdiPlusMinusVariant, mdiPodiumGold, mdiPulse, mdiRefresh, mdiResize, mdiRestore, mdiScaleBalance, mdiSort, mdiSortAscending, mdiSortDescending, mdiSourceBranch, mdiSourceCommit, mdiSourceRepository, mdiTransition, mdiUndo } from "@mdi/js";
10
10
  import clsx$1, { clsx } from "clsx";
11
11
  import { ArrowContainer, Popover } from "react-tiny-popover";
12
12
  import { HexColorPicker } from "react-colorful";
@@ -14,6 +14,8 @@ import { clean, compare, valid } from "semver";
14
14
  import colorConvert from "color-convert";
15
15
  import { getLuminance } from "a11y-contrast-color";
16
16
  import { twMerge } from "tailwind-merge";
17
+ import { create } from "zustand";
18
+ import { createSerializer, parseAsBoolean, parseAsInteger, parseAsNumberLiteral, parseAsString, parseAsStringLiteral, useQueryState, useQueryStates } from "nuqs";
17
19
  import * as d3 from "d3";
18
20
  import { interpolateCool, scaleOrdinal, scaleSequential, schemeTableau10 } from "d3";
19
21
  import sha1 from "sha1";
@@ -30,7 +32,6 @@ import { existsSync as existsSync$1, promises as promises$1, readFileSync } from
30
32
  import os$1, { cpus, freemem, totalmem } from "node:os";
31
33
  import { inflateSync } from "node:zlib";
32
34
  import { readFile, readdir } from "node:fs/promises";
33
- import { createSerializer, parseAsBoolean, parseAsInteger, parseAsNumberLiteral, parseAsString, parseAsStringLiteral, useQueryState, useQueryStates } from "nuqs";
34
35
  import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
35
36
  import randomstring from "randomstring";
36
37
  import { createPortal } from "react-dom";
@@ -180,7 +181,7 @@ function handleBrowserRequest(request, responseStatusCode, responseHeaders, reac
180
181
  });
181
182
  }
182
183
  const name = "git-truck";
183
- const version = "3.2.0";
184
+ const version = "3.3.0";
184
185
  const description = "Visualizing a Git repository";
185
186
  const main = "./cli.mjs";
186
187
  const bin = "./cli.mjs";
@@ -251,7 +252,8 @@ const dependencies = {
251
252
  "semver": "^7.7.4",
252
253
  "sha1": "^1.1.1",
253
254
  "tailwind-merge": "^3.4.0",
254
- "yargs-parser": "^22.0.0"
255
+ "yargs-parser": "^22.0.0",
256
+ "zustand": "^5.0.11"
255
257
  };
256
258
  const devDependencies = {
257
259
  "@eslint/js": "^9.39.2",
@@ -297,7 +299,7 @@ const devDependencies = {
297
299
  "react-router-devtools": "^6.2.0",
298
300
  "react-use-size": "^5.0.1",
299
301
  "tailwindcss": "^4.1.18",
300
- "tsdown": "^0.16.8",
302
+ "tsdown": "^0.21.0",
301
303
  "tsx": "^4.21.0",
302
304
  "typescript": "^5.9.3",
303
305
  "typescript-eslint": "^8.55.0",
@@ -882,10 +884,13 @@ function usePrefersLightMode() {
882
884
  }
883
885
  const noEntryColor = "#c0c0c0";
884
886
  const missingInMapColor = "#808080";
887
+ const MULTIPLE_CONTRIBUTORS = "Multiple contributors";
885
888
  const feature_flags = {
886
889
  newLastChangedColorScheme: true,
887
890
  lastChangedAsGrad: false,
888
- straightTextForBlobsInCircularLayout: true
891
+ straightTextForBlobsInCircularLayout: true,
892
+ show_legend_highlight: false,
893
+ group_noncode_filetypes_in_other: true
889
894
  };
890
895
  const ABAP = {
891
896
  "type": "programming",
@@ -8369,54 +8374,323 @@ function useMetrics() {
8369
8374
  if (!context) throw new Error("useMetrics must be used within a MetricsContext");
8370
8375
  return context;
8371
8376
  }
8377
+ var truck_default$1 = "/assets/truck-BgAoc4Gr.gif";
8378
+ function CheckboxWithLabel({ children, checked, intermediate, onChange, className = "", checkBoxClassName = "", checkedIcon = mdiCheckboxMarked, uncheckedIcon = mdiCheckboxBlankOutline, ...props }) {
8379
+ const [value, setValue] = useState(checked);
8380
+ const [isTransitioning, startTransition$1] = useTransition();
8381
+ return /* @__PURE__ */ jsxs("label", {
8382
+ className: `label flex w-full items-center justify-start gap-2 ${className}`,
8383
+ ...props,
8384
+ children: [
8385
+ /* @__PURE__ */ jsx("input", {
8386
+ type: "checkbox",
8387
+ checked: value,
8388
+ className: "peer hidden",
8389
+ onChange: (e) => {
8390
+ setValue(e.target.checked);
8391
+ startTransition$1(() => onChange(e));
8392
+ }
8393
+ }),
8394
+ /* @__PURE__ */ jsx(Icon, {
8395
+ className: cn("peer-checked:text-blue-primary place-self-end", intermediate ? "text-blue-primary" : "text-tertiary-text dark:text-tertiary-text-dark", checkBoxClassName),
8396
+ path: intermediate ? mdiCheckboxIntermediate : value ? checkedIcon : uncheckedIcon,
8397
+ size: 1
8398
+ }),
8399
+ /* @__PURE__ */ jsx("div", {
8400
+ className: "text-secondary-text hover:text-blue-primary dark:text-secondary-text-dark flex flex-1 items-center gap-2",
8401
+ children
8402
+ }),
8403
+ /* @__PURE__ */ jsx("img", {
8404
+ src: truck_default$1,
8405
+ alt: "",
8406
+ "aria-hidden": "true",
8407
+ className: cn("h-5", { "opacity-0": !isTransitioning })
8408
+ })
8409
+ ]
8410
+ });
8411
+ }
8412
+ var useSelectionStore = create()((set) => ({
8413
+ selectedCategories: [],
8414
+ setSelectedCategories: (categories) => set({ selectedCategories: Array.from(categories) }),
8415
+ selectCategory: (label) => set((state) => {
8416
+ const newSet = new Set(state.selectedCategories);
8417
+ newSet.add(label);
8418
+ return { selectedCategories: Array.from(newSet) };
8419
+ }),
8420
+ deselectCategory: (label) => set((state) => {
8421
+ const newSet = new Set(state.selectedCategories);
8422
+ newSet.delete(label);
8423
+ return { selectedCategories: Array.from(newSet) };
8424
+ }),
8425
+ selectCategories: (labels) => set((state) => {
8426
+ const newSet = new Set(state.selectedCategories);
8427
+ for (const label of labels) newSet.add(label);
8428
+ return { selectedCategories: Array.from(newSet) };
8429
+ }),
8430
+ deselectCategories: (labels) => set((state) => {
8431
+ const newSet = new Set(state.selectedCategories);
8432
+ for (const label of labels) newSet.delete(label);
8433
+ return { selectedCategories: Array.from(newSet) };
8434
+ }),
8435
+ resetSelection: () => set({ selectedCategories: [] })
8436
+ }));
8437
+ const useSelectedCategories = () => {
8438
+ const { metricType } = useOptions();
8439
+ return useSelectionStore((state) => state.selectedCategories).filter((c$1) => c$1.startsWith(metricType + ":"));
8440
+ };
8441
+ const useSelectedCategory = () => {
8442
+ const { metricType } = useOptions();
8443
+ const selectCategory = useSelectionStore((s) => s.selectCategory);
8444
+ const deselectCategory = useSelectionStore((s) => s.deselectCategory);
8445
+ const selectedCategories = useSelectionStore((s) => s.selectedCategories).filter((c$1) => c$1.startsWith(metricType + ":"));
8446
+ const isSelected = (category) => selectedCategories.includes(`${metricType}:${category}`);
8447
+ const select = (category) => selectCategory(`${metricType}:${category}`);
8448
+ const deselect = (category) => deselectCategory(`${metricType}:${category}`);
8449
+ return {
8450
+ isSelected,
8451
+ select,
8452
+ deselect
8453
+ };
8454
+ };
8455
+ const useSelectCategories = () => {
8456
+ const { metricType } = useOptions();
8457
+ const selectCategories = useSelectionStore((s) => s.selectCategories);
8458
+ return (categories) => selectCategories(Array.from(categories, (c$1) => `${metricType}:${c$1}`));
8459
+ };
8460
+ const useDeselectCategories = () => {
8461
+ const { metricType } = useOptions();
8462
+ const deselectCategories = useSelectionStore((s) => s.deselectCategories);
8463
+ return (categories) => deselectCategories(Array.from(categories, (c$1) => `${metricType}:${c$1}`));
8464
+ };
8465
+ const useResetSelection = () => useSelectionStore((state) => state.resetSelection);
8466
+ const useIsCategorySelected = () => {
8467
+ const { metricType } = useOptions();
8468
+ const selectedCategories = useSelectionStore((s) => s.selectedCategories).filter((c$1) => c$1.startsWith(metricType + ":"));
8469
+ return (category) => selectedCategories.includes(`${metricType}:${category}`) || selectedCategories.length === 0;
8470
+ };
8471
+ function ResetSelectionButton() {
8472
+ const selectedCategories = useSelectedCategories();
8473
+ const resetSelection = useResetSelection();
8474
+ const disabled = selectedCategories.length === 0;
8475
+ return /* @__PURE__ */ jsx("button", {
8476
+ className: cn("btn btn--text btn--danger p-0", { "cursor-not-allowed opacity-50": disabled }),
8477
+ title: "Reset selection",
8478
+ disabled,
8479
+ onClick: resetSelection,
8480
+ children: "Reset selection"
8481
+ });
8482
+ }
8483
+ function PointLegendDistBar({ items, totalWeight }) {
8484
+ const { isSelected: selected, select, deselect } = useSelectedCategory();
8485
+ const CUTOFF = 2;
8486
+ const noSelectedCategories = useSelectedCategories().length === 0;
8487
+ const mapped = items.map(([label, info$1]) => {
8488
+ const percentage = totalWeight > 0 ? info$1.weight / totalWeight * 100 : 0;
8489
+ return {
8490
+ label,
8491
+ color: info$1.color,
8492
+ percentage,
8493
+ info: info$1
8494
+ };
8495
+ });
8496
+ const segments = mapped.filter((seg) => seg.percentage >= CUTOFF);
8497
+ const restWeight = mapped.filter((seg) => seg.percentage < CUTOFF).reduce((sum, seg) => sum + seg.percentage, 0);
8498
+ if (restWeight > 0) segments.push({
8499
+ label: "Rest",
8500
+ color: missingInMapColor,
8501
+ percentage: restWeight,
8502
+ info: new PointInfo(missingInMapColor, Infinity)
8503
+ });
8504
+ const representedLabels = new Set(segments.filter((seg) => seg.label !== "Rest").flatMap((seg) => [seg.label, ...Array.from(seg.info.children?.keys() ?? [])]));
8505
+ const restLabels = mapped.filter((seg) => !representedLabels.has(seg.label)).map((seg) => seg.label);
8506
+ const toggleSegmentSelection = (seg, isSel) => {
8507
+ if (seg.label === "Rest") {
8508
+ restLabels.forEach((label) => {
8509
+ if (isSel) deselect(label);
8510
+ else select(label);
8511
+ });
8512
+ return;
8513
+ }
8514
+ if (isSel) {
8515
+ deselect(seg.label);
8516
+ seg.info.children.forEach((_, childLabel) => deselect(childLabel));
8517
+ } else {
8518
+ select(seg.label);
8519
+ seg.info.children.forEach((_, childLabel) => select(childLabel));
8520
+ }
8521
+ };
8522
+ return /* @__PURE__ */ jsx("div", {
8523
+ className: "ring-primary-bg dark:ring-primary-bg-dark mt-2 flex h-3 w-full overflow-hidden rounded-4xl ring-4",
8524
+ children: segments.map((seg) => {
8525
+ const isSel = selected(seg.label) || seg.label === "Rest" && restLabels.some((label) => selected(label));
8526
+ return /* @__PURE__ */ jsx("span", {
8527
+ role: "button",
8528
+ tabIndex: 0,
8529
+ "aria-pressed": isSel,
8530
+ className: cn("h-full cursor-pointer outline-none hover:opacity-80", {
8531
+ "opacity-15 grayscale-100 hover:grayscale-0": !noSelectedCategories && !isSel,
8532
+ "opacity-100": noSelectedCategories || isSel,
8533
+ "z-10 ring-2 ring-black": isSel
8534
+ }),
8535
+ title: `${seg.label} (${seg.percentage.toFixed(1)}%)`,
8536
+ style: {
8537
+ background: seg.color,
8538
+ width: `${seg.percentage}%`
8539
+ },
8540
+ onClick: () => toggleSegmentSelection(seg, isSel),
8541
+ onKeyDown: (e) => {
8542
+ if (e.key === "Enter" || e.key === " ") {
8543
+ e.preventDefault();
8544
+ toggleSegmentSelection(seg, isSel);
8545
+ }
8546
+ }
8547
+ }, seg.label);
8548
+ })
8549
+ });
8550
+ }
8372
8551
  var legendCutoff = 8;
8373
8552
  var PointInfo = class {
8374
8553
  color;
8375
8554
  weight;
8555
+ children;
8376
8556
  constructor(color, weight) {
8377
8557
  this.color = color;
8378
8558
  this.weight = weight;
8559
+ this.children = /* @__PURE__ */ new Map();
8379
8560
  }
8380
8561
  add(value) {
8381
8562
  this.weight += value;
8382
8563
  }
8564
+ addChild(extension, child) {
8565
+ if (this.children.has(extension)) {
8566
+ this.children.get(extension)?.add(child.weight);
8567
+ return;
8568
+ } else this.children.set(extension, child);
8569
+ }
8383
8570
  };
8384
8571
  function PointLegend() {
8385
8572
  const { metricType } = useOptions();
8386
8573
  const [metricsData] = useMetrics();
8574
+ const isCategorySelected = useIsCategorySelected();
8575
+ const [path$1] = useQueryState("path");
8576
+ const resetSelection = useResetSelection();
8577
+ useEffect(() => {
8578
+ resetSelection();
8579
+ }, [path$1, resetSelection]);
8387
8580
  const metricCache = metricsData.get(metricType);
8388
8581
  if (metricCache === void 0) throw new Error("Metric cache is undefined");
8389
8582
  const [collapse, setCollapse] = useState(true);
8583
+ const [selectedSearch, setSelectedSearch] = useState("");
8390
8584
  const items = Array.from(metricCache.legend).sort(([, info1], [, info2]) => {
8391
8585
  if (info1.weight < info2.weight) return 1;
8392
8586
  if (info1.weight > info2.weight) return -1;
8393
8587
  return 0;
8394
8588
  });
8395
- const shownItems = items.slice(0, collapse ? legendCutoff : items.length);
8589
+ const totalWeight = items.reduce((sum, [, info$1]) => sum + info$1.weight, 0);
8590
+ const matchesSearch = (label) => label.toLowerCase().includes(selectedSearch.toLowerCase());
8591
+ const filteredItems = selectedSearch.length > 0 ? items.filter(([label]) => matchesSearch(label)) : items;
8592
+ const shownItems = filteredItems.slice(0, collapse ? legendCutoff : filteredItems.length);
8396
8593
  if (items.length === 0) return null;
8397
8594
  return /* @__PURE__ */ jsxs("div", {
8398
- className: "relative grid grid-cols-2 gap-2",
8399
- children: [shownItems.map(([label, info$1]) => /* @__PURE__ */ jsx(PointLegendEntry, {
8400
- label,
8401
- info: info$1
8402
- }, label)), items.length > legendCutoff ? /* @__PURE__ */ jsx(PointLegendOther, {
8403
- items: items.slice(legendCutoff),
8404
- collapse,
8405
- toggle: () => setCollapse(!collapse)
8406
- }) : null]
8595
+ className: "-ml-8 flex flex-col gap-1",
8596
+ children: [
8597
+ /* @__PURE__ */ jsx("div", {
8598
+ className: cn("border-border dark:border-border-dark flex flex-wrap gap-0.5 rounded-lg border p-2", { hidden: !feature_flags.show_legend_highlight }),
8599
+ children: items.filter(([label]) => isCategorySelected(label)).map(([label, info$1]) => /* @__PURE__ */ jsxs("div", {
8600
+ title: label,
8601
+ className: "flex max-w-[25ch] items-center gap-1 truncate text-sm",
8602
+ children: [/* @__PURE__ */ jsx(LegendDot, { dotColor: info$1.color }), label]
8603
+ }, label))
8604
+ }),
8605
+ /* @__PURE__ */ jsxs("div", {
8606
+ className: "flex flex-col gap-2",
8607
+ children: [/* @__PURE__ */ jsx(PointLegendDistBar, {
8608
+ items,
8609
+ totalWeight
8610
+ }), /* @__PURE__ */ jsxs("div", {
8611
+ className: "flex w-full justify-between gap-2",
8612
+ children: [/* @__PURE__ */ jsx(ResetSelectionButton, {}), /* @__PURE__ */ jsxs("div", {
8613
+ className: "align-center flex flex-row gap-5 text-right",
8614
+ children: [
8615
+ /* @__PURE__ */ jsx(SearchCategoriesButton, {
8616
+ selectedSearch,
8617
+ setSelectedSearch
8618
+ }),
8619
+ /* @__PURE__ */ jsx("p", {
8620
+ className: "w-12 self-center text-sm font-bold",
8621
+ children: "# Files"
8622
+ }),
8623
+ /* @__PURE__ */ jsx("p", {
8624
+ className: "w-12 self-center text-sm font-bold",
8625
+ children: "% Files"
8626
+ })
8627
+ ]
8628
+ })]
8629
+ })]
8630
+ }),
8631
+ /* @__PURE__ */ jsx("div", {
8632
+ className: "flex justify-between gap-1",
8633
+ children: /* @__PURE__ */ jsxs("div", {
8634
+ className: "flex flex-1 flex-col gap-2",
8635
+ children: [shownItems.map(([label, info$1]) => /* @__PURE__ */ jsx(PointLegendEntry, {
8636
+ label,
8637
+ info: info$1,
8638
+ totalWeight
8639
+ }, label)), filteredItems.length > legendCutoff ? /* @__PURE__ */ jsx(PointLegendOther, {
8640
+ items: filteredItems.slice(legendCutoff),
8641
+ collapse,
8642
+ toggle: () => setCollapse(!collapse)
8643
+ }) : null]
8644
+ })
8645
+ })
8646
+ ]
8407
8647
  });
8408
8648
  }
8409
- function PointLegendEntry({ label, info: info$1 }) {
8649
+ function PointLegendEntry({ label, info: info$1, totalWeight }) {
8410
8650
  const { metricType } = useOptions();
8651
+ const isAuthorRelatedLegend = metricType === "TOP_CONTRIBUTOR";
8652
+ const selectedCategories = useSelectedCategories();
8653
+ const selectCategories = useSelectCategories();
8654
+ const deselectCategories = useDeselectCategories();
8655
+ const { isSelected } = useSelectedCategory();
8656
+ const isOnlySelectedCategory = isSelected(label) && selectedCategories.length === 1;
8657
+ const noSelectedCategories = selectedCategories.length === 0;
8658
+ const labelIsSelected = isSelected(label);
8659
+ const dotColor = info$1.color;
8411
8660
  return /* @__PURE__ */ jsxs("div", {
8412
- className: "relative flex items-center gap-1 text-sm leading-none",
8413
- children: [metricType === "TOP_CONTRIBUTOR" ? /* @__PURE__ */ jsx(LegendDot, {
8414
- dotColor: info$1.color,
8415
- authorColorToChange: label
8416
- }) : /* @__PURE__ */ jsx(LegendDot, { dotColor: info$1.color }), /* @__PURE__ */ jsx("span", {
8417
- className: "truncate font-bold",
8418
- title: label,
8419
- children: label
8661
+ className: "width-full justify-content relative flex gap-1 align-middle text-sm leading-none",
8662
+ children: [/* @__PURE__ */ jsxs(CheckboxWithLabel, {
8663
+ checkBoxClassName: "opacity-0 group-hover:opacity-100 transition-opacity",
8664
+ intermediate: noSelectedCategories,
8665
+ checked: labelIsSelected,
8666
+ onChange: (evt) => {
8667
+ if (evt.target.checked) selectCategories([label, ...info$1.children ? Array.from(info$1.children.keys()) : []]);
8668
+ else deselectCategories([label, ...info$1.children ? Array.from(info$1.children.keys()) : []]);
8669
+ },
8670
+ children: [isAuthorRelatedLegend ? /* @__PURE__ */ jsx(LegendDot, {
8671
+ dotColor,
8672
+ authorColorToChange: label
8673
+ }, dotColor) : /* @__PURE__ */ jsx(LegendDot, { dotColor }, dotColor), /* @__PURE__ */ jsx("span", {
8674
+ className: cn("truncate", {
8675
+ "font-bold": true,
8676
+ "text-blue-primary": labelIsSelected,
8677
+ "italic underline": label === "Other" || label === "Multiple contributors"
8678
+ }),
8679
+ title: noSelectedCategories ? `Highlight ${label} exclusively` : isOnlySelectedCategory ? "Highlight all categories" : labelIsSelected ? `Remove ${label} from filter` : `Add ${label} to filter`,
8680
+ children: label
8681
+ })]
8682
+ }, String(labelIsSelected)), /* @__PURE__ */ jsxs("div", {
8683
+ className: "text-muted-foreground align-center center flex flex-row gap-5 text-right text-xs",
8684
+ children: [/* @__PURE__ */ jsx("span", {
8685
+ className: "self-center",
8686
+ children: info$1.weight.toLocaleString()
8687
+ }), /* @__PURE__ */ jsxs("span", {
8688
+ className: "w-12 self-center",
8689
+ children: [(info$1.weight / totalWeight * 100).toLocaleString(void 0, {
8690
+ minimumFractionDigits: 1,
8691
+ maximumFractionDigits: 1
8692
+ }), "%"]
8693
+ })]
8420
8694
  })]
8421
8695
  }, label);
8422
8696
  }
@@ -8446,20 +8720,58 @@ function PointLegendOther({ toggle, items, collapse }) {
8446
8720
  })
8447
8721
  });
8448
8722
  }
8723
+ function SearchCategoriesButton({ selectedSearch, setSelectedSearch }) {
8724
+ const ref = useRef(null);
8725
+ useKey({ key: "Escape" }, () => {
8726
+ ref.current?.blur();
8727
+ setSelectedSearch("");
8728
+ });
8729
+ return /* @__PURE__ */ jsxs("div", {
8730
+ className: "not-focus-within:has-placeholder-shown:w-button pointer-events-none right-0 z-10 flex w-full flex-col gap-2 transition-[left,width,translate] duration-75 **:pointer-events-auto not-focus-within:has-placeholder-shown:static not-focus-within:has-placeholder-shown:translate-x-0",
8731
+ children: [/* @__PURE__ */ jsx("button", {
8732
+ className: "hidden min-w-max cursor-pointer peer-placeholder-shown:hidden peer-focus:inline",
8733
+ children: /* @__PURE__ */ jsx(Icon, {
8734
+ path: mdiClose,
8735
+ size: "1em"
8736
+ })
8737
+ }), /* @__PURE__ */ jsxs("label", {
8738
+ className: "input min-h-button h-button max-h-button relative flex w-full min-w-0 cursor-pointer flex-row-reverse items-center gap-2 overflow-hidden not-focus-within:has-placeholder-shown:grow-0 not-focus-within:has-placeholder-shown:gap-0",
8739
+ title: "Search within selected",
8740
+ children: [/* @__PURE__ */ jsx("input", {
8741
+ ref,
8742
+ type: "text",
8743
+ placeholder: "Search selected…",
8744
+ value: selectedSearch,
8745
+ className: "peer w-full grow placeholder-shown:not-focus:w-0 placeholder-shown:not-focus:min-w-0",
8746
+ onChange: (e) => setSelectedSearch(e.target.value)
8747
+ }), /* @__PURE__ */ jsx(Icon, {
8748
+ path: mdiMagnify,
8749
+ className: "hidden min-w-max peer-placeholder-shown:inline peer-focus:hidden"
8750
+ })]
8751
+ })]
8752
+ });
8753
+ }
8449
8754
  function setExtensionColor(blob, cache) {
8450
- const extension = blob.name.substring(blob.name.lastIndexOf(".") + 1);
8451
- const extensionInfo = getColorFromExtension(extension);
8755
+ const extensionInfo = getColorFromExtension(blob.extension);
8452
8756
  let color = extensionInfo?.color;
8453
8757
  if (extensionInfo?.lang === "JSON") color = "#f6bf00";
8454
8758
  const legend = cache.legend;
8455
- if (color) {
8456
- if (legend.has(extension)) legend.get(extension)?.add(1);
8457
- else legend.set(extension, new PointInfo(color, 1));
8759
+ if (feature_flags.group_noncode_filetypes_in_other) if (color) {
8760
+ if (legend.has(blob.extension)) legend.get(blob.extension)?.add(1);
8761
+ else legend.set(blob.extension, new PointInfo(color, 1));
8458
8762
  cache.colormap.set(blob.path, color);
8459
8763
  } else {
8460
8764
  if (!legend.has("Other")) legend.set("Other", new PointInfo(noEntryColor, 0));
8765
+ if (legend.get("Other")?.children?.has(blob.extension)) legend.get("Other")?.children?.get(blob.extension)?.add(1);
8766
+ else legend.get("Other")?.addChild(blob.extension, new PointInfo(noEntryColor, 1));
8767
+ legend.get("Other")?.add(1);
8461
8768
  cache.colormap.set(blob.path, noEntryColor);
8462
8769
  }
8770
+ else {
8771
+ if (legend.has(blob.extension)) legend.get(blob.extension)?.add(1);
8772
+ else legend.set(blob.extension, new PointInfo(color ? color : noEntryColor, 1));
8773
+ cache.colormap.set(blob.path, color ? color : noEntryColor);
8774
+ }
8463
8775
  }
8464
8776
  function lastChangedGroupings(newestEpoch, _oldestChangeDate) {
8465
8777
  return feature_flags.newLastChangedColorScheme ? [
@@ -8574,14 +8886,21 @@ var ContribAmountTranslater = class {
8574
8886
  function setDominantAuthorColor(authorColors, blob, cache, dominantAuthorPerFile, dominantAuthorCutoff, contribSumPerFile) {
8575
8887
  const dominantAuthor = dominantAuthorPerFile[blob.path];
8576
8888
  const contribSum = contribSumPerFile[blob.path];
8577
- if (!dominantAuthor || !contribSum) return;
8578
8889
  const legend = cache.legend;
8579
- if (dominantAuthor.contribcount / contribSum * 100 < dominantAuthorCutoff) {
8890
+ const bumpMultiple = () => {
8891
+ if (legend.has("Multiple contributors")) legend.get(MULTIPLE_CONTRIBUTORS)?.add(1);
8892
+ else legend.set(MULTIPLE_CONTRIBUTORS, new PointInfo(noEntryColor, 1));
8580
8893
  cache.colormap.set(blob.path, noEntryColor);
8894
+ };
8895
+ if (!dominantAuthor || !contribSum) {
8896
+ bumpMultiple();
8897
+ return;
8898
+ }
8899
+ if (dominantAuthor.contribcount / contribSum * 100 < dominantAuthorCutoff) {
8900
+ bumpMultiple();
8581
8901
  return;
8582
8902
  }
8583
8903
  const color = authorColors[dominantAuthor.author] ?? "#c0c0c0";
8584
- legend.set("Multiple authors", new PointInfo(noEntryColor, Infinity));
8585
8904
  cache.colormap.set(blob.path, color);
8586
8905
  if (legend.has(dominantAuthor.author)) {
8587
8906
  legend.get(dominantAuthor.author)?.add(1);
@@ -8785,11 +9104,9 @@ const CloseButton = ({ className = "", absolute = true, ...props }) => /* @__PUR
8785
9104
  const LegendDot = ({ className = "", dotColor, authorColorToChange = void 0 }) => {
8786
9105
  const [color, setColor] = useState(dotColor);
8787
9106
  const { databaseInfo } = useData();
8788
- const { chartType } = useOptions();
8789
9107
  const submit = useViewSubmit();
8790
9108
  if (!authorColorToChange) return /* @__PURE__ */ jsx(Dot, {
8791
9109
  className,
8792
- chartType,
8793
9110
  color
8794
9111
  });
8795
9112
  function updateColor(author, color$1) {
@@ -8809,7 +9126,6 @@ const LegendDot = ({ className = "", dotColor, authorColorToChange = void 0 }) =
8809
9126
  ],
8810
9127
  trigger: ({ onClick }) => /* @__PURE__ */ jsx(Dot, {
8811
9128
  className: cn("cursor-pointer", className),
8812
- chartType,
8813
9129
  color: dotColor,
8814
9130
  onClick
8815
9131
  }),
@@ -8831,7 +9147,8 @@ const LegendDot = ({ className = "", dotColor, authorColorToChange = void 0 }) =
8831
9147
  ]
8832
9148
  });
8833
9149
  };
8834
- var Dot = ({ color, chartType, className = "", ...props }) => {
9150
+ var Dot = ({ color, className = "", ...props }) => {
9151
+ const { chartType } = useOptions();
8835
9152
  return /* @__PURE__ */ jsx(props.onClick ? "button" : "div", {
8836
9153
  ...props,
8837
9154
  className: cn("aspect-square h-4 w-4 shadow-xs shadow-black transition-[border-radius] duration-[10s]", chartType === "BUBBLE_CHART" ? "rounded-full" : "rounded-xs", className),
@@ -10023,6 +10340,7 @@ var ServerInstance = class {
10023
10340
  hash: child.hash,
10024
10341
  path: newPath,
10025
10342
  name: newName,
10343
+ extension: newName.substring(newName.lastIndexOf(".") + 1),
10026
10344
  sizeInBytes: child.size
10027
10345
  };
10028
10346
  currTree.children.push(blob);
@@ -10329,7 +10647,6 @@ var InstanceManager = class {
10329
10647
  }
10330
10648
  };
10331
10649
  var truck_default = "data:image/png;base64,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";
10332
- var truck_default$1 = "/assets/truck-BgAoc4Gr.gif";
10333
10650
  function UpdateNotifier({ installedVersion, latestVersion: latestVersion$1 }) {
10334
10651
  const isExperimental = installedVersion.includes("0.0.0");
10335
10652
  return /* @__PURE__ */ jsxs(Popover$1, {
@@ -10955,7 +11272,7 @@ var browse_default = UNSAFE_withComponentProps(function Index() {
10955
11272
  search: value,
10956
11273
  offset: 0
10957
11274
  }))
10958
- }, path$1),
11275
+ }, "search-input" + path$1),
10959
11276
  /* @__PURE__ */ jsx("label", {
10960
11277
  className: "label",
10961
11278
  htmlFor: "path",
@@ -10980,7 +11297,7 @@ var browse_default = UNSAFE_withComponentProps(function Index() {
10980
11297
  evt.currentTarget.value = updatePathIfChanged(evt.currentTarget.value);
10981
11298
  }
10982
11299
  }
10983
- }, parentDirectoryPath),
11300
+ }, "path-input" + parentDirectoryPath),
10984
11301
  error$1 ? /* @__PURE__ */ jsx("output", {
10985
11302
  className: "truncate text-sm text-red-600 dark:text-red-400",
10986
11303
  title: error$1,
@@ -11187,7 +11504,7 @@ function DirectoryEntry({ entry: entry$1, status, isAnalyzed }) {
11187
11504
  children: status === "Error" || !entry$1.lastChanged ? "-" : (/* @__PURE__ */ new Date(entry$1.lastChanged * 1e3)).toLocaleDateString()
11188
11505
  })]
11189
11506
  })
11190
- }), /* @__PURE__ */ jsx("hr", { className: "col-span-full opacity-50 last:hidden" })] }, entry$1.path);
11507
+ }), /* @__PURE__ */ jsx("hr", { className: "col-span-full opacity-50 last:hidden" })] });
11191
11508
  }
11192
11509
  function DirectoryEntryPlaceholder() {
11193
11510
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
@@ -11432,23 +11749,26 @@ const Chart = memo(function Chart$2({ hoveredObject, setHoveredObject }) {
11432
11749
  const size = useDeferredValue(rawSize);
11433
11750
  const { databaseInfo } = useData();
11434
11751
  const { chartType, sizeMetric, hierarchyType, labelsVisible, renderCutOff } = useOptions();
11435
- const [params, setParams] = useQueryStates(viewSearchParamsConfig);
11436
- const { zoomPath } = params;
11437
- const setZoomPath = (zoomPathUpdate) => setParams((prev) => {
11438
- const value = typeof zoomPathUpdate === "function" ? zoomPathUpdate(prev.zoomPath) : zoomPathUpdate;
11439
- return {
11440
- ...prev,
11441
- zoomPath: value && value !== databaseInfo.repo ? trimFilenameFromPath(value) : null
11442
- };
11443
- });
11752
+ const isCategorySelected = useIsCategorySelected();
11753
+ const [params] = useQueryStates(viewSearchParamsConfig);
11754
+ const [zoomPath, setZoomPathRaw] = useQueryState("zoomPath");
11755
+ const setZoomPath = (value) => {
11756
+ return setZoomPathRaw(value && value !== databaseInfo.repo ? trimFilenameFromPath(value) : null);
11757
+ };
11444
11758
  const sep = zoomPath ? zoomPath?.includes("/") ? "/" : "\\" : null;
11445
11759
  const zoomOneLevelOut = () => {
11446
11760
  if (!sep || !zoomPath) return;
11447
11761
  setZoomPath(zoomPath.split(sep).slice(0, -1).join(sep));
11448
11762
  };
11449
11763
  const { clickedObject, setClickedObject } = useClickedObject();
11450
- const { showFilesWithoutChanges, showOnlySearchMatches } = useOptions();
11764
+ const { dominantAuthorCutoff, metricType, showFilesWithoutChanges, showOnlySearchMatches } = useOptions();
11451
11765
  const navigate = useNavigate();
11766
+ useKey({ key: "Escape" }, () => {
11767
+ if (clickedObject) navigate(href("/view") + viewSerializer({
11768
+ ...params,
11769
+ objectPath: null
11770
+ }), { state: { clickedObject: null } });
11771
+ });
11452
11772
  const tabURL = useMatch(href("/view/commits")) ? "/view/commits" : "/view/details";
11453
11773
  const filetree = useMemo(() => {
11454
11774
  const ig = ignore();
@@ -11510,7 +11830,7 @@ const Chart = memo(function Chart$2({ hoveredObject, setHoveredObject }) {
11510
11830
  const createGroupHandlers = (d) => {
11511
11831
  const onClick = (evt) => {
11512
11832
  evt.stopPropagation();
11513
- if (clickedObject) {
11833
+ if (clickedObject && d && clickedObject.path === d.data.path) {
11514
11834
  navigate(href("/view") + viewSerializer({
11515
11835
  ...params,
11516
11836
  objectPath: null
@@ -11583,12 +11903,21 @@ const Chart = memo(function Chart$2({ hoveredObject, setHoveredObject }) {
11583
11903
  children: nodes.map((d) => {
11584
11904
  const isSearchMatch = Boolean(searchResults[d.data.path]);
11585
11905
  const eventHandlers = createGroupHandlers(d);
11906
+ const extension = d.data.name.substring(d.data.name.lastIndexOf(".") + 1);
11907
+ let topContributor = MULTIPLE_CONTRIBUTORS;
11908
+ const dominant = databaseInfo.dominantAuthors[d.data.path];
11909
+ const contribSum = databaseInfo.contribSumPerFile[d.data.path];
11910
+ const authorPercentage = dominant ? dominant.contribcount / contribSum * 100 : null;
11911
+ if (authorPercentage !== null && authorPercentage >= dominantAuthorCutoff) topContributor = dominant.author;
11912
+ const category = metricType === "FILE_TYPE" ? extension : metricType === "TOP_CONTRIBUTOR" ? topContributor : null;
11913
+ const isSelected = category ? isCategorySelected(category) : true;
11914
+ const shouldColor = clickedObject ? d.data.path === clickedObject.path || isTree(clickedObject) && d.data.path.startsWith(clickedObject.path + "/") : isSelected;
11915
+ const shouldNotColor = hasSearchResults && !isSearchMatch || !shouldColor;
11586
11916
  return /* @__PURE__ */ jsxs("g", {
11587
- className: cn("duration-400", {
11588
- "cursor-pointer": !clickedObject,
11917
+ className: cn("cursor-pointer duration-400", {
11589
11918
  "hover:opacity-80": isBlob(d.data) && !clickedObject,
11590
11919
  "hover:stroke-border-highlight dark:hover:stroke-border-highlight-dark": isTree(d.data) && !clickedObject,
11591
- "opacity-30": !isSearchMatch && hasSearchResults || clickedObject?.type === "blob" && clickedObject.path !== d.data.path
11920
+ "opacity-30 grayscale hover:opacity-100 hover:grayscale-0": shouldNotColor
11592
11921
  }),
11593
11922
  ...eventHandlers,
11594
11923
  children: [/* @__PURE__ */ jsx(Node, { d }), labelsVisible ? /* @__PURE__ */ jsx(NodeText, {
@@ -11647,10 +11976,10 @@ function Node({ d }) {
11647
11976
  const { chartType, metricType, transitionsEnabled } = useOptions();
11648
11977
  return /* @__PURE__ */ jsx("rect", {
11649
11978
  ...useMemo(() => {
11650
- let props = { ...isBlob(d.data) ? {
11979
+ let props = isBlob(d.data) ? {
11651
11980
  fill: metricsData.get(metricType)?.colormap.get(d.data.path) ?? "#808080",
11652
11981
  stroke: metricsData.get(metricType)?.colormap.get(d.data.path) ?? "#c0c0c0"
11653
- } : { strokeWidth: "1px" } };
11982
+ } : {};
11654
11983
  if (chartType === "BUBBLE_CHART") {
11655
11984
  const circleDatum = d;
11656
11985
  props = {
@@ -11823,40 +12152,6 @@ function flatten(tree) {
11823
12152
  else flattened.push(...flatten(child));
11824
12153
  return flattened;
11825
12154
  }
11826
- function CheckboxWithLabel({ children, checked, onChange, className = "", checkedIcon = mdiCheckboxOutline, uncheckedIcon = mdiCheckboxBlankOutline, ...props }) {
11827
- const [value, setValue] = useState(checked);
11828
- const [isTransitioning, startTransition$1] = useTransition();
11829
- return /* @__PURE__ */ jsxs("label", {
11830
- className: `label group flex w-full items-center justify-start gap-2 hover:text-blue-500 ${className}`,
11831
- ...props,
11832
- children: [
11833
- /* @__PURE__ */ jsx("div", {
11834
- className: "flex flex-1 items-center gap-2",
11835
- children
11836
- }),
11837
- /* @__PURE__ */ jsx("img", {
11838
- src: truck_default$1,
11839
- alt: "",
11840
- "aria-hidden": "true",
11841
- className: cn("h-5", { "opacity-0": !isTransitioning })
11842
- }),
11843
- /* @__PURE__ */ jsx(Icon, {
11844
- className: "place-self-end group-hover:text-blue-500",
11845
- path: value ? checkedIcon : uncheckedIcon,
11846
- size: 1
11847
- }),
11848
- /* @__PURE__ */ jsx("input", {
11849
- type: "checkbox",
11850
- checked: value,
11851
- className: "hidden",
11852
- onChange: (e) => {
11853
- setValue(e.target.checked);
11854
- startTransition$1(() => onChange(e));
11855
- }
11856
- })
11857
- ]
11858
- });
11859
- }
11860
12155
  function GroupAuthorsModal() {
11861
12156
  const { databaseInfo } = useData();
11862
12157
  const submit = useViewSubmit();
@@ -13481,11 +13776,11 @@ function Tooltip({ className = "", hoveredObject }) {
13481
13776
  function ColorMetricDependentInfo(props) {
13482
13777
  let icon;
13483
13778
  let content;
13484
- const slicedPath = props.hoveredBlob?.path ?? "";
13779
+ const path$1 = props.hoveredBlob?.path ?? "";
13485
13780
  switch (props.metric) {
13486
13781
  case "MOST_COMMITS": {
13487
13782
  icon = mdiSourceCommit;
13488
- const noCommits = props.databaseInfo.commitCounts[slicedPath];
13783
+ const noCommits = props.databaseInfo.commitCounts[path$1];
13489
13784
  if (!noCommits) {
13490
13785
  content = "No activity in selected range";
13491
13786
  break;
@@ -13495,7 +13790,7 @@ function ColorMetricDependentInfo(props) {
13495
13790
  }
13496
13791
  case "LAST_CHANGED": {
13497
13792
  icon = mdiPulse;
13498
- const epoch = props.databaseInfo.lastChanged[slicedPath];
13793
+ const epoch = props.databaseInfo.lastChanged[path$1];
13499
13794
  if (!epoch) {
13500
13795
  content = "No activity in selected range";
13501
13796
  break;
@@ -13505,8 +13800,8 @@ function ColorMetricDependentInfo(props) {
13505
13800
  }
13506
13801
  case "TOP_CONTRIBUTOR": {
13507
13802
  icon = mdiAccount;
13508
- const dominant = props.databaseInfo.dominantAuthors[slicedPath];
13509
- const contribSum = props.databaseInfo.contribSumPerFile[slicedPath];
13803
+ const dominant = props.databaseInfo.dominantAuthors[path$1];
13804
+ const contribSum = props.databaseInfo.contribSumPerFile[path$1];
13510
13805
  if (!dominant) {
13511
13806
  content = "No activity in selected range";
13512
13807
  break;
@@ -13518,7 +13813,7 @@ function ColorMetricDependentInfo(props) {
13518
13813
  const authorPercentage = Math.round(dominant.contribcount / contribSum * 100);
13519
13814
  if (authorPercentage < props.dominantAuthorCutoff) {
13520
13815
  icon = mdiAccountGroup;
13521
- content = "Multiple contributors";
13816
+ content = MULTIPLE_CONTRIBUTORS;
13522
13817
  break;
13523
13818
  }
13524
13819
  content = `${dominant.author} ${authorPercentage}%`;
@@ -13526,7 +13821,7 @@ function ColorMetricDependentInfo(props) {
13526
13821
  }
13527
13822
  case "MOST_CONTRIBUTIONS": {
13528
13823
  icon = mdiPlusMinusVariant;
13529
- const contribs = props.databaseInfo.contribSumPerFile[slicedPath];
13824
+ const contribs = props.databaseInfo.contribSumPerFile[path$1];
13530
13825
  if (!contribs) {
13531
13826
  content = "No activity in selected range";
13532
13827
  break;
@@ -13701,6 +13996,19 @@ function ResetTimeIntervalButton() {
13701
13996
  })]
13702
13997
  });
13703
13998
  }
13999
+ function ClickedObjectButton() {
14000
+ const { clickedObject, setClickedObject } = useClickedObject();
14001
+ const data = useDataNullable();
14002
+ if (!clickedObject || !data) return null;
14003
+ return /* @__PURE__ */ jsxs("button", {
14004
+ className: "btn btn--primary",
14005
+ title: "Deselect clicked object",
14006
+ onClick: () => {
14007
+ setClickedObject(null);
14008
+ },
14009
+ children: [/* @__PURE__ */ jsx(Icon, { path: mdiClose }), clickedObject.name]
14010
+ });
14011
+ }
13704
14012
  var view_exports = /* @__PURE__ */ __export({
13705
14013
  action: () => action,
13706
14014
  currentRepositoryContext: () => currentRepositoryContext,
@@ -14033,7 +14341,7 @@ var view_default = UNSAFE_withComponentProps(function Repo() {
14033
14341
  children: /* @__PURE__ */ jsx(Options, {})
14034
14342
  }),
14035
14343
  /* @__PURE__ */ jsx(CollapsibleHeader, {
14036
- className: "card",
14344
+ className: "card group",
14037
14345
  title: "Legend",
14038
14346
  contentClassName: "pb-6",
14039
14347
  children: /* @__PURE__ */ jsx(Legend, { hoveredObject })
@@ -14062,7 +14370,10 @@ var view_default = UNSAFE_withComponentProps(function Repo() {
14062
14370
  /* @__PURE__ */ jsx(Breadcrumb, { zoom: true })
14063
14371
  ]
14064
14372
  }),
14065
- /* @__PURE__ */ jsx("div", { className: "flex gap-2" }),
14373
+ /* @__PURE__ */ jsx("div", {
14374
+ className: "flex gap-2",
14375
+ children: /* @__PURE__ */ jsx(ClickedObjectButton, {})
14376
+ }),
14066
14377
  /* @__PURE__ */ jsxs("div", {
14067
14378
  className: "flex gap-2",
14068
14379
  children: [
@@ -14501,6 +14812,7 @@ var view_details_default = UNSAFE_withComponentProps(function Details() {
14501
14812
  }), isBlob$1 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Form, {
14502
14813
  className: "w-max",
14503
14814
  method: "post",
14815
+ action: viewAction,
14504
14816
  children: [/* @__PURE__ */ jsx("input", {
14505
14817
  type: "hidden",
14506
14818
  name: "hide",
@@ -14514,6 +14826,7 @@ var view_details_default = UNSAFE_withComponentProps(function Details() {
14514
14826
  }), clickedObject.name.includes(".") ? /* @__PURE__ */ jsxs(Form, {
14515
14827
  className: "w-max",
14516
14828
  method: "post",
14829
+ action: viewAction,
14517
14830
  children: [/* @__PURE__ */ jsx("input", {
14518
14831
  type: "hidden",
14519
14832
  name: "hide",
@@ -15210,6 +15523,7 @@ var ui_default = UNSAFE_withComponentProps(function UI() {
15210
15523
  name: "demo.txt",
15211
15524
  path: "src/demo.txt",
15212
15525
  hash: "abc123",
15526
+ extension: "txt",
15213
15527
  sizeInBytes: 42
15214
15528
  } })]
15215
15529
  })]
@@ -15245,14 +15559,14 @@ var server_manifest_default = {
15245
15559
  "hasClientMiddleware": false,
15246
15560
  "hasDefaultExport": true,
15247
15561
  "hasErrorBoundary": true,
15248
- "module": "/assets/root-CGqKHKfX.js",
15562
+ "module": "/assets/root-BBgpPS0e.js",
15249
15563
  "imports": [
15250
15564
  "/assets/jsx-runtime-s_YwNCZb.js",
15251
15565
  "/assets/react-dom-B9ui1jR3.js",
15252
- "/assets/dist-Bpk5WHYg.js",
15253
- "/assets/clear-cache-BJQQzFka.js"
15566
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15567
+ "/assets/clear-cache-C8-c8bzY.js"
15254
15568
  ],
15255
- "css": ["/assets/root-BK0u4vgl.css"],
15569
+ "css": ["/assets/root-BDi1Y-Zk.css"],
15256
15570
  "clientActionModule": void 0,
15257
15571
  "clientLoaderModule": void 0,
15258
15572
  "clientMiddlewareModule": void 0,
@@ -15292,12 +15606,12 @@ var server_manifest_default = {
15292
15606
  "hasClientMiddleware": false,
15293
15607
  "hasDefaultExport": true,
15294
15608
  "hasErrorBoundary": false,
15295
- "module": "/assets/clear-cache-DkN7P69Z.js",
15609
+ "module": "/assets/clear-cache-DczfzTH1.js",
15296
15610
  "imports": [
15297
15611
  "/assets/jsx-runtime-s_YwNCZb.js",
15298
- "/assets/dist-Bpk5WHYg.js",
15612
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15299
15613
  "/assets/react-dom-B9ui1jR3.js",
15300
- "/assets/clear-cache-BJQQzFka.js"
15614
+ "/assets/clear-cache-C8-c8bzY.js"
15301
15615
  ],
15302
15616
  "css": [],
15303
15617
  "clientActionModule": void 0,
@@ -15381,11 +15695,11 @@ var server_manifest_default = {
15381
15695
  "hasClientMiddleware": false,
15382
15696
  "hasDefaultExport": true,
15383
15697
  "hasErrorBoundary": false,
15384
- "module": "/assets/browse-Bx9O5Mvl.js",
15698
+ "module": "/assets/browse-BPS_2Q2o.js",
15385
15699
  "imports": [
15386
15700
  "/assets/jsx-runtime-s_YwNCZb.js",
15387
- "/assets/browse-B9wBPRaK.js",
15388
- "/assets/dist-Bpk5WHYg.js",
15701
+ "/assets/browse-DtF-O2nh.js",
15702
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15389
15703
  "/assets/react-dom-B9ui1jR3.js"
15390
15704
  ],
15391
15705
  "css": [],
@@ -15428,11 +15742,11 @@ var server_manifest_default = {
15428
15742
  "hasClientMiddleware": false,
15429
15743
  "hasDefaultExport": true,
15430
15744
  "hasErrorBoundary": false,
15431
- "module": "/assets/view-mmd0Q4B8.js",
15745
+ "module": "/assets/view-tqZGjwcM.js",
15432
15746
  "imports": [
15433
15747
  "/assets/jsx-runtime-s_YwNCZb.js",
15434
- "/assets/browse-B9wBPRaK.js",
15435
- "/assets/dist-Bpk5WHYg.js",
15748
+ "/assets/browse-DtF-O2nh.js",
15749
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15436
15750
  "/assets/react-dom-B9ui1jR3.js"
15437
15751
  ],
15438
15752
  "css": [],
@@ -15475,13 +15789,13 @@ var server_manifest_default = {
15475
15789
  "hasClientMiddleware": false,
15476
15790
  "hasDefaultExport": true,
15477
15791
  "hasErrorBoundary": false,
15478
- "module": "/assets/view.commits-b-k-A7BT.js",
15792
+ "module": "/assets/view.commits-CsXn1fsk.js",
15479
15793
  "imports": [
15480
15794
  "/assets/jsx-runtime-s_YwNCZb.js",
15481
- "/assets/browse-B9wBPRaK.js",
15482
- "/assets/dist-Bpk5WHYg.js",
15795
+ "/assets/browse-DtF-O2nh.js",
15796
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15483
15797
  "/assets/react-dom-B9ui1jR3.js",
15484
- "/assets/RepoTabs-36qUc-RT.js"
15798
+ "/assets/RepoTabs-CAVKlzaP.js"
15485
15799
  ],
15486
15800
  "css": [],
15487
15801
  "clientActionModule": void 0,
@@ -15502,13 +15816,13 @@ var server_manifest_default = {
15502
15816
  "hasClientMiddleware": false,
15503
15817
  "hasDefaultExport": true,
15504
15818
  "hasErrorBoundary": false,
15505
- "module": "/assets/view.details-B7LCPu46.js",
15819
+ "module": "/assets/view.details-DurbDplo.js",
15506
15820
  "imports": [
15507
15821
  "/assets/jsx-runtime-s_YwNCZb.js",
15508
- "/assets/browse-B9wBPRaK.js",
15509
- "/assets/dist-Bpk5WHYg.js",
15822
+ "/assets/browse-DtF-O2nh.js",
15823
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15510
15824
  "/assets/react-dom-B9ui1jR3.js",
15511
- "/assets/RepoTabs-36qUc-RT.js"
15825
+ "/assets/RepoTabs-CAVKlzaP.js"
15512
15826
  ],
15513
15827
  "css": [],
15514
15828
  "clientActionModule": void 0,
@@ -15550,11 +15864,11 @@ var server_manifest_default = {
15550
15864
  "hasClientMiddleware": false,
15551
15865
  "hasDefaultExport": true,
15552
15866
  "hasErrorBoundary": false,
15553
- "module": "/assets/ui-Bw635ygf.js",
15867
+ "module": "/assets/ui-mrRo7bBy.js",
15554
15868
  "imports": [
15555
15869
  "/assets/jsx-runtime-s_YwNCZb.js",
15556
- "/assets/browse-B9wBPRaK.js",
15557
- "/assets/dist-Bpk5WHYg.js",
15870
+ "/assets/browse-DtF-O2nh.js",
15871
+ "/assets/GitTruckInfo-DSTHQLpb.js",
15558
15872
  "/assets/react-dom-B9ui1jR3.js"
15559
15873
  ],
15560
15874
  "css": [],
@@ -15585,8 +15899,8 @@ var server_manifest_default = {
15585
15899
  "hydrateFallbackModule": void 0
15586
15900
  }
15587
15901
  },
15588
- "url": "/assets/manifest-601d0239.js",
15589
- "version": "601d0239",
15902
+ "url": "/assets/manifest-708752f7.js",
15903
+ "version": "708752f7",
15590
15904
  "sri": void 0
15591
15905
  };
15592
15906
  const assetsBuildDirectory = "build/client";