Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,22 @@ native `<input type=color>`, no Acrylic or Accent opacity slider. Spec and plan:
Explorer's MEDIUM row is the tree's default row (`rowLook()` in `treePrefs`). The `marquee`
e2e measures both; a drag in a test is taken by the row's NAME, since a locator's centre is
blank space.
**IN THE EXPLORER THE WHOLE ROW DRAGS, THE ARROW STAYS, AND THE BOX STARTS ONLY OFF THE ROWS**
(#320; owner, 2026-10-07: "when you left click drag an item dont switch the cursor to the hand,
keep it the normal cursor ... the whole row should let me left click drag ... that drag should
only be from empty spaces either under or beside the file row"). This narrows #257 for the
Explorer's list (the tree keeps its rule): a press ANYWHERE on a file row (any cell, any gap)
is its click and drag, never a sweep; the sweep starts under the last row or BESIDE the rows.
For "beside" a row is as wide as its columns (`width: fit-content`), Name grows only to
`--browse-name-max` (440px, Large 520px; search's Path to `--browse-path-max`) instead of the
rest of the width, and a `--browse-gutter` (32px) on the right is always blank, so even a
narrow list has a place to start one; the header keeps the same grid and gutter, so its labels
stay over the row's cells, and its LAST cell's box still runs on to the header's right edge
(`--browse-col-edge-end`; the 2026-10-04 "no strip a hover cannot fill" rule).
A click beside the rows clears the marks like the space under them. During an internal drag
every element's cursor is pinned to `default` (`internalFileDrag.ts`; it was `grabbing`).
The `marquee` e2e measures the gutter and the header, sweeps from beside, drags from the Size
and Date cells (label up, no rectangle, `default` cursor under the pointer, on html and body).
**MARKING IS NOT PICKING** (#263; owner, 2026-10-03: "when you multiselect like this it picks
a file so here this drag starts one of the videos ... same is the case if i ctrl select it
shouldnt start or preview anything"). In the Explorer a sweep and a Ctrl or Shift click call
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "prism",
"version": "0.95.1",
"version": "0.95.2",
"description": "A fast, universal media viewer for Windows.",
"author": "Max",
"license": "MIT",
Expand Down
47 changes: 23 additions & 24 deletions src/renderer/src/components/browse/BrowseList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { DRAG_MIME, setDrag } from '../../lib/dragDrop'
import { FolderIcon, KindIcon, SweepBand, iconColour } from '../TreeRows'
import { OverlayScrollbar } from './OverlayScrollbar'
import { explorerHeadVars, explorerRow, useExplorerSize } from '../../lib/explorerSize'
import { bandBox, nearestRow, onRowOwnPart, rowsInBand } from '../../lib/marquee'
import { bandBox, nearestRow, rowsInBand } from '../../lib/marquee'
import { useSweep } from '../../hooks/useSweep'
import { BrowseIcon } from './BrowseIcon'
import { useFolderDrop } from './useFolderDrop'
Expand Down Expand Up @@ -186,12 +186,19 @@ export function BrowseList(props: Props): JSX.Element {
rowAt(first + index)
)
/**
* THE SWEEP (#257). From the list's blank space (under the rows, or a row
* to the right of its name, the other columns included) a drag draws the
* rectangle and marks every row it touches, live. A press on a row's icon
* or name is still the file's own drag, so a file still drags out to other
* apps. Rows are found by index, never by element: only the rows in view
* exist, and a row scrolled away under the rectangle is still in it.
* THE SWEEP (#257). From the list's blank space a drag draws the rectangle
* and marks every row it touches, live. Rows are found by index, never by
* element: only the rows in view exist, and a row scrolled away under the
* rectangle is still in it.
*
* THE WHOLE ROW IS THE FILE'S (#320; owner, 2026-10-07: "its not possible
* to pick up items unless you left click drag when hovering over the file
* name. the whole row should let me left click drag ... that drag should
* only be from empty spaces either under or beside the file row"). Until
* then a row's Type, Size and Date cells swept. Now a press anywhere on a
* row drags it, and the sweep starts only off the rows: under the last one,
* or beside them, since a row ends where its last column does and a gutter
* is always left on the right (browse.css), File Explorer's Details view.
*/
const [sweeping, setSweeping] = useState<{ paths: string[]; add: boolean } | null>(null)
const sweepAdd = useRef(false)
Expand Down Expand Up @@ -249,16 +256,10 @@ export function BrowseList(props: Props): JSX.Element {
})
const onListPointerDown = (e: ReactPointerEvent<HTMLDivElement>): void => {
if (e.button !== 0 || props.loading || props.message || !props.onSweep) return
const el = e.target as HTMLElement
const row = el.closest<HTMLElement>('.browse-row')
if (row) {
// The row up to the end of its name is the file's: it drags it, as
// before. The other columns are blank space and sweep.
const name = row.querySelectorAll('.browse-name > svg, .browse-name-text')
if (onRowOwnPart(e.clientX, [...name].map((n) => n.getBoundingClientRect()))) return
}
// A file's row, every cell of it and the gaps between: its own drag.
if ((e.target as HTMLElement).closest('.browse-row[data-browse-path]')) return
sweepAdd.current = e.ctrlKey
sweep.begin(e, row)
sweep.begin(e)
}
/** What reads as marked: the sweep in progress (plus, for Ctrl, what was
* marked before it), else the selection FolderBrowser holds. */
Expand Down Expand Up @@ -546,7 +547,10 @@ export function BrowseList(props: Props): JSX.Element {
// the preview plays (owner, 2026-10-03: "i should have to click the
// video or the pause icon"). A right press on an unmarked row too.
onClick={(e) => {
if (e.target === e.currentTarget) props.onSelect(null, true)
// The space beside the rows (#320) is the row layer's own box.
const at = e.target as HTMLElement
if (at === e.currentTarget || at.matches('.browse-row-space, .browse-row-layer'))
props.onSelect(null, true)
}}
onContextMenu={(e) => {
// The empty space's own menu, inside an archive (#300). A row's
Expand All @@ -562,14 +566,9 @@ export function BrowseList(props: Props): JSX.Element {
{props.message}
</div>
) : (
<div
className="browse-row-space"
style={{ height: spaceHeight }}
onClick={(e) => {
if (e.target === e.currentTarget) props.onSelect(null, true)
}}
>
<div className="browse-row-space" style={{ height: spaceHeight }}>
<div
className="browse-row-layer"
style={{
transform: `translateY(${props.scrollTop + first * rowHeight - logicalTop}px)`
}}
Expand Down
16 changes: 8 additions & 8 deletions src/renderer/src/components/browse/archive.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@
right-aligned like Size's. The narrow layouts hide it before Type, the old
panel's order. */
.browse-list-area[data-in-archive] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(160px, 1fr) 128px 76px 76px 136px;
grid-template-columns: minmax(160px, var(--browse-name-max)) 128px 76px 76px 136px;
}
.browse-list-area[data-row-size='large'][data-in-archive] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(160px, 1fr) 150px 90px 90px 160px;
grid-template-columns: minmax(160px, var(--browse-name-max)) 150px 90px 90px 160px;
}
.browse-row .browse-column-packed {
text-align: right;
Expand All @@ -89,13 +89,13 @@
@container (max-width: 720px) {
.browse-list-area[data-in-archive] :is(.browse-columns, .browse-row),
.browse-list-area[data-row-size='large'][data-in-archive] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(120px, 1fr) 116px 72px 72px;
grid-template-columns: minmax(120px, var(--browse-name-max)) 116px 72px 72px;
}
.browse-list-area[data-in-archive] .browse-columns .browse-column-size {
--browse-col-edge-r: calc(var(--browse-col-gap) / 2);
}
.browse-list-area[data-in-archive] .browse-columns .browse-column-packed {
--browse-col-edge-r: var(--browse-row-pad, 8px);
--browse-col-edge-r: var(--browse-col-edge-end);
}
}
@container (max-width: 600px) {
Expand All @@ -104,22 +104,22 @@
}
.browse-list-area[data-in-archive] :is(.browse-columns, .browse-row),
.browse-list-area[data-row-size='large'][data-in-archive] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(120px, 1fr) 116px 72px;
grid-template-columns: minmax(120px, var(--browse-name-max)) 116px 72px;
}
.browse-list-area[data-in-archive] .browse-columns .browse-column-size {
--browse-col-edge-r: var(--browse-row-pad, 8px);
--browse-col-edge-r: var(--browse-col-edge-end);
}
}
@container (max-width: 510px) {
.browse-list-area[data-in-archive] :is(.browse-columns, .browse-row),
.browse-list-area[data-row-size='large'][data-in-archive] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(110px, 1fr) 72px;
grid-template-columns: minmax(110px, var(--browse-name-max)) 72px;
}
}
@container (max-width: 300px) {
.browse-list-area[data-in-archive] :is(.browse-columns, .browse-row),
.browse-list-area[data-row-size='large'][data-in-archive] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: minmax(0, var(--browse-name-max));
}
}

Expand Down
55 changes: 40 additions & 15 deletions src/renderer/src/components/browse/browse.css
Original file line number Diff line number Diff line change
Expand Up @@ -543,17 +543,39 @@ body[data-internal-file-drag] .browse-path:focus-within {
and the columns narrowed with its text; the header sits a few pixels taller
than a row, in the same text size. Large puts back the Explorer as it was
before #257, columns and all (below). */
/* THE ROWS END WHERE THE COLUMNS DO (#320; owner, 2026-10-07: the box select
"should only be from empty spaces either under or beside the file row").
A press anywhere on a row drags it, so the list needs blank space beside
the rows for the rectangle, as File Explorer's Details view has: Name
grows only to --browse-name-max (no longer the rest of the width), a row
is as wide as its columns and no wider, and a gutter on the right is
always left blank, so even a narrow list has somewhere to start a sweep.
The header keeps the same grid and the same gutter, so every label stays
over its column; its LAST cell's box still runs on to the header's right
edge (`--browse-col-edge-end`, clipped by the header's viewport), the
2026-10-04 rule below: no strip of header a hover cannot fill. */
.browse-list-area {
--browse-name-max: 440px;
--browse-path-max: 560px;
--browse-gutter: 32px;
--browse-col-edge-end: calc(var(--browse-row-pad, 8px) + var(--browse-gutter) + 100vw);
}
.browse-list-area[data-row-size='large'] {
--browse-name-max: 520px;
--browse-path-max: 640px;
}
.browse-columns,
.browse-row {
display: grid;
grid-template-columns: minmax(160px, 1fr) 128px 76px 136px;
grid-template-columns: minmax(160px, var(--browse-name-max)) 128px 76px 136px;
align-items: center;
/* A variable, so the header's cells can reach half of it each way. */
--browse-col-gap: 12px;
column-gap: var(--browse-col-gap);
padding: 0 var(--browse-row-pad, 8px);
}
.browse-columns {
padding-right: calc(var(--browse-row-pad, 8px) + var(--browse-gutter));
flex-shrink: 0;
/* `explorerHeadHeight`: a row plus 6px, Large's 36px. */
height: var(--browse-head-h, calc(var(--browse-row-h, 26px) + 6px));
Expand All @@ -572,7 +594,8 @@ body[data-internal-file-drag] .browse-path:focus-within {
effect"). The header keeps the rows' grid, so every label stays over its
column; each cell then reaches out by half the gap on both sides, and the
first and last by the side padding, so the cells tile the header from edge
to edge and the fill covers all of a cell. The last cell is set again
to edge and the fill covers all of a cell. The last one runs on past the
end of the rows (#320) to the header's edge. The last cell is set again
below wherever a narrow layout hides a column. */
.browse-columns button {
--browse-col-edge-l: calc(var(--browse-col-gap) / 2);
Expand All @@ -591,7 +614,7 @@ body[data-internal-file-drag] .browse-path:focus-within {
--browse-col-edge-l: var(--browse-row-pad, 8px);
}
.browse-columns button:last-child {
--browse-col-edge-r: var(--browse-row-pad, 8px);
--browse-col-edge-r: var(--browse-col-edge-end);
}
.folder-browser .browse-columns button:active {
background: color-mix(in srgb, var(--p-text) 10%, transparent);
Expand Down Expand Up @@ -733,7 +756,9 @@ body[data-internal-file-drag] .browse-path:focus-within {
background: var(--p-divider);
}
.browse-row {
width: 100%;
/* As wide as its columns, never into the gutter (#320, above). */
width: fit-content;
max-width: calc(100% - var(--browse-gutter));
height: var(--browse-row-h, 26px);
text-align: left;
color: var(--p-text-soft);
Expand Down Expand Up @@ -861,11 +886,11 @@ body[data-internal-file-drag] .browse-path:focus-within {
}
.browse-columns,
.browse-row {
grid-template-columns: minmax(120px, 1fr) 116px 72px;
grid-template-columns: minmax(120px, var(--browse-name-max)) 116px 72px;
}
/* Date modified is gone, so Size is the last cell and reaches the edge. */
.browse-columns .browse-column-size {
--browse-col-edge-r: var(--browse-row-pad, 8px);
--browse-col-edge-r: var(--browse-col-edge-end);
}
}
@container (max-width: 510px) {
Expand All @@ -874,7 +899,7 @@ body[data-internal-file-drag] .browse-path:focus-within {
}
.browse-columns,
.browse-row {
grid-template-columns: minmax(110px, 1fr) 72px;
grid-template-columns: minmax(110px, var(--browse-name-max)) 72px;
--browse-col-gap: 10px;
}
}
Expand All @@ -884,10 +909,10 @@ body[data-internal-file-drag] .browse-path:focus-within {
}
.browse-columns,
.browse-row {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: minmax(0, var(--browse-name-max));
}
.browse-columns .browse-column-name {
--browse-col-edge-r: var(--browse-row-pad, 8px);
--browse-col-edge-r: var(--browse-col-edge-end);
}
}
@container (max-width: 690px) {
Expand Down Expand Up @@ -920,7 +945,7 @@ body[data-internal-file-drag] .browse-path:focus-within {

/* Search keeps the regular row height and dedicates the middle column to location. */
.browse-list-area[data-searching] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 76px;
grid-template-columns: minmax(0, var(--browse-name-max)) minmax(0, var(--browse-path-max)) 76px;
}
.browse-list-area[data-searching] :is(.browse-columns, .browse-row-space) {
min-width: 470px;
Expand All @@ -941,7 +966,7 @@ body[data-internal-file-drag] .browse-path:focus-within {
15px names. The row, text, icon and padding come in through --browse-row-*;
these are the parts a variable does not reach. */
.browse-list-area[data-row-size='large'] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(160px, 1fr) 150px 90px 160px;
grid-template-columns: minmax(160px, var(--browse-name-max)) 150px 90px 160px;
--browse-col-gap: 16px;
}
.browse-list-area[data-row-size='large'] .browse-columns {
Expand All @@ -957,22 +982,22 @@ body[data-internal-file-drag] .browse-path:focus-within {
}
@container (max-width: 720px) {
.browse-list-area[data-row-size='large'] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(120px, 1fr) 130px 80px;
grid-template-columns: minmax(120px, var(--browse-name-max)) 130px 80px;
}
}
@container (max-width: 510px) {
.browse-list-area[data-row-size='large'] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(110px, 1fr) 80px;
grid-template-columns: minmax(110px, var(--browse-name-max)) 80px;
--browse-col-gap: 10px;
}
}
@container (max-width: 300px) {
.browse-list-area[data-row-size='large'] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: minmax(0, var(--browse-name-max));
}
}
.browse-list-area[data-row-size='large'][data-searching] :is(.browse-columns, .browse-row) {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 90px;
grid-template-columns: minmax(0, var(--browse-name-max)) minmax(0, var(--browse-path-max)) 90px;
}
/* NO LOADING SCREEN (#271). A folder that has not answered keeps the rows it
had; past 120 ms they dim, so a slow read does not look like the old
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/src/lib/internalFileDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function useInternalFileDrag(stepTab: (delta: number) => void): void {
pressed = false
cancelAnimationFrame(frame)
delete document.body.dataset.internalFileDrag
delete document.documentElement.dataset.internalFileDrag
setDrag(null)
// A release over a file must not also select/open the drop destination.
suppressClick = true
Expand Down Expand Up @@ -129,9 +130,13 @@ export function useInternalFileDrag(stepTab: (delta: number) => void): void {
const label =
paths.length === 1 ? (paths[0].split(/[\\/]/).pop() ?? 'Item') : `${paths.length} items`
badge.textContent = label
// THE ARROW STAYS (#320; owner, 2026-10-07: "when you left click drag an
// item dont switch the cursor to the hand, keep it the normal cursor").
// It was `grabbing`. Pinned to the arrow for the whole drag, so nothing
// under the pointer (a link, a text field, a splitter) changes it either.
const cursorStyle = document.createElement('style')
cursorStyle.textContent =
'[data-internal-file-drag], [data-internal-file-drag] * { cursor: grabbing !important; }'
'html[data-internal-file-drag], html[data-internal-file-drag] * { cursor: default !important; }'
document.head.append(cursorStyle)
Object.assign(badge.style, {
position: 'fixed',
Expand Down Expand Up @@ -165,6 +170,7 @@ export function useInternalFileDrag(stepTab: (delta: number) => void): void {
pin: !!pin
}
document.body.dataset.internalFileDrag = 'true'
document.documentElement.dataset.internalFileDrag = 'true'
hover()
frame = requestAnimationFrame(tick)
}
Expand Down
Loading
Loading