release(v1.9.3): unify folder icons across tree & strip, add “paper” lines, live color sync, and vendor-aware release
This commit is contained in:
61
.github/workflows/release-on-version.yml
vendored
61
.github/workflows/release-on-version.yml
vendored
@@ -21,15 +21,10 @@ permissions:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Only run on:
|
||||
# - push (master + version.js path filter already enforces that)
|
||||
# - manual dispatch
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|
||||
# Duplicate safety; also step "Skip if tag exists" will no-op if already released.
|
||||
concurrency:
|
||||
group: release-${{ github.event_name }}-${{ github.run_id }}
|
||||
cancel-in-progress: false
|
||||
@@ -46,17 +41,14 @@ jobs:
|
||||
else
|
||||
REF_IN="master"
|
||||
fi
|
||||
# Resolve to a commit sha (allow branches or shas)
|
||||
if git ls-remote --exit-code --heads https://github.com/${{ github.repository }}.git "$REF_IN" >/dev/null 2>&1; then
|
||||
REF="$REF_IN"
|
||||
else
|
||||
# Accept SHAs too; we’ll let checkout validate
|
||||
REF="$REF_IN"
|
||||
fi
|
||||
else
|
||||
REF="${{ github.sha }}"
|
||||
fi
|
||||
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
echo "Using ref=$REF"
|
||||
|
||||
@@ -75,7 +67,6 @@ jobs:
|
||||
if [[ -n "${{ github.event.inputs.version || '' }}" ]]; then
|
||||
VER="${{ github.event.inputs.version }}"
|
||||
else
|
||||
# Parse APP_VERSION from public/js/version.js (expects vX.Y.Z)
|
||||
if [[ ! -f public/js/version.js ]]; then
|
||||
echo "public/js/version.js not found; cannot auto-detect version." >&2
|
||||
exit 1
|
||||
@@ -86,7 +77,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected version: $VER"
|
||||
|
||||
@@ -124,20 +114,67 @@ jobs:
|
||||
./ staging/
|
||||
bash ./scripts/stamp-assets.sh "${VER}" "$(pwd)/staging"
|
||||
|
||||
- name: Verify placeholders removed
|
||||
# --- PHP + Composer for vendor/ (production) ---
|
||||
- name: Setup PHP
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
id: php
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
tools: composer:v2
|
||||
extensions: mbstring, json, curl, dom, fileinfo, openssl, zip
|
||||
coverage: none
|
||||
ini-values: memory_limit=-1
|
||||
|
||||
- name: Cache Composer downloads
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.composer/cache
|
||||
~/.cache/composer
|
||||
key: composer-${{ runner.os }}-php-${{ steps.php.outputs.php-version }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
composer-${{ runner.os }}-php-${{ steps.php.outputs.php-version }}-
|
||||
|
||||
- name: Install PHP dependencies into staging
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
env:
|
||||
COMPOSER_MEMORY_LIMIT: -1
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pushd staging >/dev/null
|
||||
if [[ -f composer.json ]]; then
|
||||
composer install \
|
||||
--no-dev \
|
||||
--prefer-dist \
|
||||
--no-interaction \
|
||||
--no-progress \
|
||||
--optimize-autoloader \
|
||||
--classmap-authoritative
|
||||
test -f vendor/autoload.php || (echo "Composer install did not produce vendor/autoload.php" >&2; exit 1)
|
||||
else
|
||||
echo "No composer.json in staging; skipping vendor install."
|
||||
fi
|
||||
popd >/dev/null
|
||||
# --- end Composer ---
|
||||
|
||||
- name: Verify placeholders removed (skip vendor/)
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ROOT="$(pwd)/staging"
|
||||
if grep -R -n -E "{{APP_QVER}}|{{APP_VER}}" "$ROOT" \
|
||||
--exclude-dir=vendor --exclude-dir=vendor-bin \
|
||||
--include='*.html' --include='*.php' --include='*.css' --include='*.js' 2>/dev/null; then
|
||||
echo "Unreplaced placeholders found in staging." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: No unreplaced placeholders."
|
||||
|
||||
- name: Zip artifact
|
||||
- name: Zip artifact (includes vendor/)
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 11/11/2025 (v1.9.3)
|
||||
|
||||
release(v1.9.3): unify folder icons across tree & strip, add “paper” lines, live color sync, and vendor-aware release
|
||||
|
||||
- UI / Icons
|
||||
- Replace Material icon in folder strip with shared `folderSVG()` and export it for reuse. Adds clipPaths, subtle gradients, and `shape-rendering: geometricPrecision` to eliminate the tiny seam.
|
||||
- Add ruled “paper” lines and blue handwriting dashes; CSS for `.paper-line` and `.paper-ink` included.
|
||||
- Match strokes between tree (24px) and strip (48px) so both look identical; round joins/caps to avoid nicks.
|
||||
- Polish folder strip layout & hover: tighter spacing, centered icon+label, improved wrapping.
|
||||
|
||||
- Folder color & non-empty detection
|
||||
- Live color sync: after saving a color we dispatch `folderColorChanged`; strip repaints and tree refreshes.
|
||||
- Async strip icon: paint immediately, then flip to “paper” if the folder has contents. HSL helpers compute front/back/stroke shades.
|
||||
|
||||
- FileList strip
|
||||
- Render subfolders with `<span class="folder-svg">` + name, wire context menu actions (move, color, share, etc.), and attach icons for each tile.
|
||||
|
||||
- Exports & helpers
|
||||
- Export `openColorFolderModal(...)` and `openMoveFolderUI(...)` for the strip and toolbar; use `refreshFolderIcon(...)` after ops to keep icons current.
|
||||
|
||||
- AppCore
|
||||
- Update file upload DnD relay hook to `#fileList` (id rename).
|
||||
|
||||
- CSS tweaks
|
||||
- Bring tree icon stroke/paint rules in line with the strip, add scribble styles, and adjust margins/spacing.
|
||||
|
||||
- CI/CD (release)
|
||||
- Build PHP dependencies during release: setup PHP 8.3 + Composer, cache downloads, install into `staging/vendor/`, exclude `vendor/` from placeholder checks, and ship artifact including `vendor/`.
|
||||
|
||||
- Changelog highlights
|
||||
- Sharper, seam-free folder SVGs shared across tree & strip, with paper lines + handwriting accents.
|
||||
- Real-time folder color propagation between views.
|
||||
- Folder strip switched to SVG tiles with better layout + context actions.
|
||||
- Release pipeline now produces a ready-to-run zip that includes `vendor/`.
|
||||
|
||||
---
|
||||
|
||||
## Changes 11/10/2025 (v1.9.2)
|
||||
|
||||
release(v1.9.2): Upload modal + DnD relay from file list (with robust synthetic-drop fallback)
|
||||
|
||||
@@ -739,11 +739,167 @@ body {
|
||||
width: 100% !important;
|
||||
}#fileList table tr:nth-child(even) {
|
||||
background-color: transparent;
|
||||
}#fileList table tr:hover {
|
||||
background-color: #e0e0e0;
|
||||
}.dark-mode #fileList table tr:hover {
|
||||
background-color: #444;
|
||||
}#fileListTitle {
|
||||
}
|
||||
/* --- File list rows: match folder-tree hover/selected --- */
|
||||
:root {
|
||||
--filr-row-hover-bg: rgba(122,179,255,.14);
|
||||
--filr-row-selected-bg: rgba(122,179,255,.24);
|
||||
}
|
||||
|
||||
/* Let cell corners round like a pill */
|
||||
#fileList table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* ===== Reset any conflicting backgrounds (Bootstrap etc.) inside #fileList only ===== */
|
||||
#fileList table tbody tr,
|
||||
#fileList table tbody tr > td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Kill Bootstrap hover/zebra just for this table */
|
||||
#fileList table.table-hover tbody tr:hover > * { background-color: transparent !important; }
|
||||
#fileList table.table-striped > tbody > tr:nth-of-type(odd) > * { background-color: transparent !important; }
|
||||
|
||||
/* ===== Our look, scoped to the table we tagged in JS ===== */
|
||||
#fileList table.filr-table tbody tr,
|
||||
#fileList table.filr-table tbody td {
|
||||
transition: background-color .12s ease;
|
||||
}
|
||||
|
||||
/* Hover (when not selected) */
|
||||
#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td {
|
||||
background: var(--filr-row-hover-bg) !important;
|
||||
}
|
||||
|
||||
/* Selected (support a few legacy class names just in case) */
|
||||
#fileList table.filr-table tbody tr.selected > td,
|
||||
#fileList table.filr-table tbody tr.row-selected > td,
|
||||
#fileList table.filr-table tbody tr.selected-row > td,
|
||||
#fileList table.filr-table tbody tr.is-selected > td {
|
||||
background: var(--filr-row-selected-bg) !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(122,179,255,.45);
|
||||
}
|
||||
|
||||
/* Rounded “pill” ends on hover/selected */
|
||||
#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:first-child,
|
||||
#fileList table.filr-table tbody tr.selected > td:first-child,
|
||||
#fileList table.filr-table tbody tr.row-selected > td:first-child,
|
||||
#fileList table.filr-table tbody tr.selected-row > td:first-child,
|
||||
#fileList table.filr-table tbody tr.is-selected > td:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:last-child,
|
||||
#fileList table.filr-table tbody tr.selected > td:last-child,
|
||||
#fileList table.filr-table tbody tr.row-selected > td:last-child,
|
||||
#fileList table.filr-table tbody tr.selected-row > td:last-child,
|
||||
#fileList table.filr-table tbody tr.is-selected > td:last-child {
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* Keyboard focus visibility */
|
||||
#fileList table.filr-table tbody tr:focus-within > td {
|
||||
outline: 2px solid #8ab4f8;
|
||||
outline-offset: -2px;
|
||||
border-top-left-radius: 8px; border-bottom-left-radius: 8px;
|
||||
border-top-right-radius: 8px; border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.dark-mode #fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td {
|
||||
background: var(--filr-row-hover-bg) !important;
|
||||
}
|
||||
.dark-mode #fileList table.filr-table tbody tr.selected > td,
|
||||
.dark-mode #fileList table.filr-table tbody tr.row-selected > td,
|
||||
.dark-mode #fileList table.filr-table tbody tr.selected-row > td,
|
||||
.dark-mode #fileList table.filr-table tbody tr.is-selected > td {
|
||||
background: var(--filr-row-selected-bg) !important;
|
||||
}
|
||||
|
||||
#fileList table.filr-table {
|
||||
--bs-table-hover-color: inherit;
|
||||
--bs-table-striped-color: inherit;
|
||||
}
|
||||
|
||||
#fileList table.table-hover tbody tr:hover,
|
||||
#fileList table.table-hover tbody tr:hover > td {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.dark-mode #fileList table.filr-table tbody td a,
|
||||
.dark-mode #fileList table.filr-table tbody td a:hover {
|
||||
color: inherit !important;
|
||||
}
|
||||
:root{
|
||||
--filr-row-outline: rgba(122,179,255,.45);
|
||||
--filr-row-outline-hover: rgba(122,179,255,.35);
|
||||
}
|
||||
|
||||
#fileList table.filr-table > :not(caption) > * > * { border: 0 !important; }
|
||||
#fileList table.filr-table td,
|
||||
#fileList table.filr-table th { box-shadow: none !important; }
|
||||
|
||||
#fileList table.filr-table tbody tr.selected > td,
|
||||
#fileList table.filr-table tbody tr.row-selected > td,
|
||||
#fileList table.filr-table tbody tr.selected-row > td,
|
||||
#fileList table.filr-table tbody tr.is-selected > td {
|
||||
background: var(--filr-row-selected-bg) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 var(--filr-row-outline),
|
||||
inset 0 -1px 0 var(--filr-row-outline);
|
||||
}
|
||||
#fileList table.filr-table tbody tr.selected > td:first-child,
|
||||
#fileList table.filr-table tbody tr.row-selected > td:first-child,
|
||||
#fileList table.filr-table tbody tr.selected-row > td:first-child,
|
||||
#fileList table.filr-table tbody tr.is-selected > td:first-child {
|
||||
box-shadow:
|
||||
inset 1px 0 0 var(--filr-row-outline),
|
||||
inset 0 1px 0 var(--filr-row-outline),
|
||||
inset 0 -1px 0 var(--filr-row-outline);
|
||||
border-top-left-radius: 8px; border-bottom-left-radius: 8px;
|
||||
}
|
||||
#fileList table.filr-table tbody tr.selected > td:last-child,
|
||||
#fileList table.filr-table tbody tr.row-selected > td:last-child,
|
||||
#fileList table.filr-table tbody tr.selected-row > td:last-child,
|
||||
#fileList table.filr-table tbody tr.is-selected > td:last-child {
|
||||
box-shadow:
|
||||
inset -1px 0 0 var(--filr-row-outline),
|
||||
inset 0 1px 0 var(--filr-row-outline),
|
||||
inset 0 -1px 0 var(--filr-row-outline);
|
||||
border-top-right-radius: 8px; border-bottom-right-radius: 8px;
|
||||
|
||||
}
|
||||
|
||||
#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td {
|
||||
background: var(--filr-row-hover-bg) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 var(--filr-row-outline-hover),
|
||||
inset 0 -1px 0 var(--filr-row-outline-hover);
|
||||
}
|
||||
#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:first-child {
|
||||
box-shadow:
|
||||
inset 1px 0 0 var(--filr-row-outline-hover),
|
||||
inset 0 1px 0 var(--filr-row-outline-hover),
|
||||
inset 0 -1px 0 var(--filr-row-outline-hover);
|
||||
border-top-left-radius: 8px; border-bottom-left-radius: 8px;
|
||||
}
|
||||
#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:last-child {
|
||||
box-shadow:
|
||||
inset -1px 0 0 var(--filr-row-outline-hover),
|
||||
inset 0 1px 0 var(--filr-row-outline-hover),
|
||||
inset 0 -1px 0 var(--filr-row-outline-hover);
|
||||
border-top-right-radius: 8px; border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
#fileList table.filr-table tbody tr:focus-within > td { outline: none; }
|
||||
#fileList table.filr-table tbody tr:focus-within > td:first-child,
|
||||
#fileList table.filr-table tbody tr:focus-within > td:last-child {
|
||||
outline: 2px solid #8ab4f8; outline-offset: -2px;
|
||||
}
|
||||
|
||||
#fileListTitle {
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
@@ -1000,7 +1156,7 @@ body {
|
||||
#fileListTitle {
|
||||
font-size: 1.8em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
}.file-list-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1185,7 +1341,7 @@ body {
|
||||
}.folder-tree.expanded {
|
||||
display: block;
|
||||
}.folder-item {
|
||||
margin: 4px 0;
|
||||
margin: 2px 0;
|
||||
display: block;
|
||||
}.folder-toggle {
|
||||
cursor: pointer;
|
||||
@@ -1432,8 +1588,6 @@ body {
|
||||
}.dark-mode table {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
}.dark-mode table tr:hover {
|
||||
background-color: #444;
|
||||
}.dark-mode #uploadProgressContainer .progress {
|
||||
background-color: #333;
|
||||
}.dark-mode #uploadProgressContainer .progress-bar {
|
||||
@@ -1876,34 +2030,74 @@ body {
|
||||
font-weight: 500;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}.folder-strip-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.folder-strip-container {
|
||||
display: flex;
|
||||
padding-top: 0px !important;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}.folder-strip-container .folder-item {
|
||||
display: flex;
|
||||
gap: 10px 14px;
|
||||
align-content: flex-start; /* multi-line wrap stays top-aligned */
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.folder-strip-container .folder-item {
|
||||
display: flex;
|
||||
padding-top: 0px !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: 80px;
|
||||
align-items: center; /* horizontal (cross-axis) center */
|
||||
justify-content: center; /* vertical (main-axis) center */
|
||||
min-width: 0;
|
||||
gap: 2px !important;
|
||||
padding: 6px 8px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
color: inherit;
|
||||
font-size: 0.85em;
|
||||
}.folder-strip-container .folder-item i.material-icons {
|
||||
font-size: 28px;
|
||||
margin-bottom: 4px;
|
||||
}.folder-strip-container .folder-name {
|
||||
transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease;
|
||||
}
|
||||
.folder-strip-container .folder-item .folder-svg {
|
||||
line-height: 0;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.folder-strip-container .folder-item .folder-svg svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: block;
|
||||
}
|
||||
.folder-strip-container .folder-name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
max-width: 80px;
|
||||
margin-top: 4px;
|
||||
}.folder-strip-container .folder-item i.material-icons {
|
||||
color: currentColor;
|
||||
}.folder-strip-container .folder-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}:root {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.folder-strip-container .folder-item:hover {
|
||||
transform: translateY(-1px) scale(1.04);
|
||||
background-color: rgba(0, 0, 0, 0.04); /* light mode */
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
/* Dark mode hover */
|
||||
body.dark-mode .folder-strip-container .folder-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .45);
|
||||
}
|
||||
|
||||
/* Optional: keyboard focus */
|
||||
.folder-strip-container .folder-item:focus-visible {
|
||||
outline: 2px solid #8ab4f8;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
:root {
|
||||
--perm-caret: #444;
|
||||
}/* light */
|
||||
.dark-mode {
|
||||
@@ -2005,170 +2199,217 @@ body {
|
||||
|
||||
#downloadProgressBarOuter { height: 10px; }
|
||||
|
||||
/* ===== FileRise Folder Tree: unified, crisp, aligned ===== */
|
||||
|
||||
/* Knobs (size, spacing, colors) */
|
||||
/* ===== Folder Tree – theme + structure ===== */
|
||||
#folderTreeContainer {
|
||||
/* Colors (used in BOTH themes) */
|
||||
--filr-folder-front: #f6b84e; /* front/lip */
|
||||
--filr-folder-back: #ffd36e; /* back body */
|
||||
--filr-folder-stroke:#a87312; /* outline */
|
||||
--filr-paper-fill: #ffffff; /* paper */
|
||||
--filr-paper-stroke: #b2c2db; /* paper edges/lines */
|
||||
|
||||
/* Size & spacing */
|
||||
--row-h: 28px; /* row height */
|
||||
--twisty: 24px; /* chevron hit-area size */
|
||||
--twisty-gap: -5px; /* gap between chevron and row content */
|
||||
--icon-size: 24px; /* 22–26 look good */
|
||||
--icon-gap: 6px; /* space between icon and label */
|
||||
--indent: 10px; /* subtree indent */
|
||||
}
|
||||
|
||||
/* Keep the same yellow/orange in dark mode; boost paper contrast a touch */
|
||||
.dark-mode #folderTreeContainer {
|
||||
/* Theme vars (light mode defaults) */
|
||||
--filr-folder-front: #f6b84e;
|
||||
--filr-folder-back: #ffd36e;
|
||||
--filr-folder-stroke:#a87312;
|
||||
--filr-paper-fill: #ffffff;
|
||||
--filr-paper-stroke: #d0def7; /* brighter so it pops on dark */
|
||||
--filr-paper-stroke: #9fb3d6; /* slightly darker for sharper paper */
|
||||
|
||||
/* Sizes */
|
||||
--row-h: 28px;
|
||||
--twisty: 24px;
|
||||
--twisty-gap: -5px;
|
||||
--icon-size: 24px;
|
||||
--icon-gap: 6px;
|
||||
--indent: 10px;
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-item { position: static; padding-left: 0; }
|
||||
|
||||
/* visible “row” for each node */
|
||||
#folderTreeContainer .folder-item { position: static; padding-left: 0; }
|
||||
#folderTreeContainer .folder-item > .folder-tree { margin-left: var(--indent); }
|
||||
|
||||
#folderTreeContainer .folder-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--row-h);
|
||||
min-height: var(--row-h);
|
||||
height: auto;
|
||||
padding-left: calc(var(--twisty) + var(--twisty-gap));
|
||||
}
|
||||
|
||||
/* children indent */
|
||||
#folderTreeContainer .folder-item > .folder-tree { margin-left: var(--indent); }
|
||||
|
||||
/* ---------- Chevron toggle (twisty) ---------- */
|
||||
|
||||
#folderTreeContainer .folder-row > button.folder-toggle {
|
||||
/* Chevron + spacer (centered vertically) */
|
||||
#folderTreeContainer .folder-row > button.folder-toggle,
|
||||
#folderTreeContainer .folder-row > .folder-spacer {
|
||||
position: absolute; left: 0; top: 50%; transform: translateY(-50%);
|
||||
width: var(--twisty); height: var(--twisty);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border: 1px solid transparent; border-radius: 6px;
|
||||
background: transparent; cursor: pointer;
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-row > button.folder-toggle::before {
|
||||
content: "▸"; /* closed */
|
||||
font-size: calc(var(--twisty) * 0.8);
|
||||
line-height: 1;
|
||||
content: "▸"; font-size: calc(var(--twisty) * 0.8); line-height: 1;
|
||||
}
|
||||
|
||||
#folderTreeContainer li[role="treeitem"][aria-expanded="true"]
|
||||
> .folder-row > button.folder-toggle::before { content: "▾"; }
|
||||
|
||||
/* root row (it's a <div>) */
|
||||
> .folder-row > button.folder-toggle::before,
|
||||
#rootRow[aria-expanded="true"] > button.folder-toggle::before { content: "▾"; }
|
||||
|
||||
#folderTreeContainer .folder-row > button.folder-toggle:hover {
|
||||
border-color: color-mix(in srgb, #7ab3ff 35%, transparent);
|
||||
}
|
||||
|
||||
/* spacer for leaves so labels align with parents that have a button */
|
||||
#folderTreeContainer .folder-row > .folder-spacer {
|
||||
position: absolute; left: 0; top: 50%; transform: translateY(-50%);
|
||||
width: var(--twisty); height: var(--twisty); display: inline-block;
|
||||
}
|
||||
|
||||
/* Row "pill" that hugs content and wraps */
|
||||
#folderTreeContainer .folder-option {
|
||||
display: inline-flex;
|
||||
flex: 0 1 auto; /* don't stretch full width */
|
||||
align-items: center;
|
||||
height: var(--row-h);
|
||||
line-height: 1.2; /* avoids baseline weirdness */
|
||||
gap: var(--icon-gap);
|
||||
height: auto;
|
||||
min-height: var(--row-h);
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
line-height: 1.2;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
gap: var(--icon-gap);
|
||||
white-space: normal; /* allow wrapping */
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transform: translateY(0.5px); /* tiny optical nudge for text */
|
||||
}
|
||||
|
||||
/* ---------- Icon box (size & alignment) ---------- */
|
||||
|
||||
#folderTreeContainer .folder-icon {
|
||||
flex: 0 0 var(--icon-size);
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateY(0.5px); /* tiny optical nudge for SVG */
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
shape-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
/* ---------- Crisp colors & strokes for the SVG parts ---------- */
|
||||
|
||||
|
||||
#folderTreeContainer .folder-icon .paper {
|
||||
fill: var(--filr-paper-fill);
|
||||
stroke: var(--filr-paper-stroke);
|
||||
stroke-width: 1.5; /* thick so it reads at 24px */
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-icon .paper-fold {
|
||||
fill: var(--filr-paper-stroke);
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-icon .paper-line {
|
||||
stroke: var(--filr-paper-stroke);
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* subtle highlight along lip to add depth */
|
||||
#folderTreeContainer .folder-icon .lip-highlight {
|
||||
stroke: #ffffff;
|
||||
stroke-opacity: .35;
|
||||
stroke-width: 0.9;
|
||||
fill: none;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
/* ---------- Hover / Selected ---------- */
|
||||
|
||||
#folderTreeContainer .folder-option:hover {
|
||||
background: rgba(122,179,255,.14);
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-option.selected {
|
||||
background: rgba(122,179,255,.24);
|
||||
box-shadow: inset 0 0 0 1px rgba(122,179,255,.45);
|
||||
}
|
||||
|
||||
/* variables will be set inline per .folder-option when user colors a folder */
|
||||
/* Label must be shrinkable so wrapping works */
|
||||
#folderTreeContainer .folder-label {
|
||||
flex: 1 1 120px;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Icon box */
|
||||
#folderTreeContainer .folder-icon {
|
||||
flex: 0 0 var(--icon-size);
|
||||
width: var(--icon-size); height: var(--icon-size);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
#folderTreeContainer .folder-icon svg {
|
||||
width: 100%; height: 100%; display: block;
|
||||
shape-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
|
||||
/* Make folder tree outline match folder strip */
|
||||
#folderTreeContainer .folder-icon .folder-front,
|
||||
#folderTreeContainer .folder-icon .folder-back {
|
||||
fill: currentColor;
|
||||
stroke: var(--filr-folder-stroke);
|
||||
stroke-width: 1.1;
|
||||
stroke-width: .5;
|
||||
paint-order: fill stroke;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
#folderTreeContainer .folder-icon .folder-front { color: var(--filr-folder-front); }
|
||||
#folderTreeContainer .folder-icon .folder-back { color: var(--filr-folder-back); }
|
||||
|
||||
#folderTreeContainer .folder-icon .paper {
|
||||
fill: var(--filr-paper-fill);
|
||||
stroke: var(--filr-paper-stroke);
|
||||
stroke-width: 1.5;
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
#folderTreeContainer .folder-icon .paper-fold { fill: var(--filr-paper-stroke); }
|
||||
#folderTreeContainer .folder-icon .paper-line {
|
||||
stroke: var(--filr-paper-stroke); stroke-width: 1.5;
|
||||
stroke-linecap: round; fill: none; opacity: .95;
|
||||
}
|
||||
#folderTreeContainer .folder-icon .lip-highlight {
|
||||
stroke: #ffffff; stroke-opacity: .35; stroke-width: .9;
|
||||
fill: none; vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-icon,
|
||||
#folderTreeContainer .folder-label { transform: none !important; }
|
||||
|
||||
/* ===== File List Strip – color the shared folderSVG() ===== */
|
||||
.folder-strip-container .folder-svg svg {
|
||||
display: block;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
.folder-strip-container .folder-item {
|
||||
/* defaults — overridden per-tile via inline CSS vars set in JS */
|
||||
--filr-folder-front: #f6b84e;
|
||||
--filr-folder-back: #ffd36e;
|
||||
--filr-folder-stroke: #a87312;
|
||||
}
|
||||
.folder-strip-container .folder-svg .folder-front,
|
||||
.folder-strip-container .folder-svg .folder-back {
|
||||
fill: currentColor;
|
||||
stroke: var(--filr-folder-stroke);
|
||||
stroke-width: .5;
|
||||
paint-order: fill stroke;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.folder-strip-container .folder-svg .folder-front { color: var(--filr-folder-front); }
|
||||
.folder-strip-container .folder-svg .folder-back { color: var(--filr-folder-back); }
|
||||
|
||||
.folder-strip-container .folder-svg .paper {
|
||||
fill: #fff;
|
||||
stroke: #b2c2db; /* light mode */
|
||||
stroke-width: 1;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.folder-strip-container .folder-svg .paper-fold { fill: #b2c2db; }
|
||||
.folder-strip-container .folder-svg .paper-line {
|
||||
stroke: #b2c2db; stroke-width: 1; stroke-linecap: round; fill: none;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.folder-strip-container .folder-svg .lip-highlight {
|
||||
stroke: rgba(255,255,255,.45); stroke-width: .8; fill: none;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
#folderTreeContainer .folder-icon .folder-front,
|
||||
#folderTreeContainer .folder-icon .folder-back,
|
||||
.folder-strip-container .folder-svg .folder-front,
|
||||
.folder-strip-container .folder-svg .folder-back,
|
||||
#folderTreeContainer .folder-icon .lip-highlight,
|
||||
.folder-strip-container .folder-svg .lip-highlight {
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
/* Make sure we’re not forcing crispEdges anywhere */
|
||||
.folder-strip-container .folder-svg svg,
|
||||
#folderTreeContainer .folder-icon svg { shape-rendering: geometricPrecision !important; }
|
||||
|
||||
@media (max-resolution: 1.5dppx) {
|
||||
#folderTreeContainer .folder-icon .folder-front,
|
||||
#folderTreeContainer .folder-icon .folder-back { stroke-width: .6; }
|
||||
}
|
||||
|
||||
|
||||
/* Scribble (the handwriting line) */
|
||||
#folderTreeContainer .folder-icon .paper-ink,
|
||||
.folder-strip-container .folder-svg .paper-ink {
|
||||
stroke: #4da3ff;
|
||||
stroke-width: .9;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
opacity: .85;
|
||||
paint-order: normal;
|
||||
|
||||
}
|
||||
|
||||
/* tree @ 24px icon */
|
||||
#folderTreeContainer .folder-icon .folder-front,
|
||||
#folderTreeContainer .folder-icon .folder-back,
|
||||
#folderTreeContainer .folder-icon .paper-line,
|
||||
#folderTreeContainer .folder-icon .paper-ink,
|
||||
#folderTreeContainer .folder-icon .lip-highlight { stroke-width: .6px; }
|
||||
|
||||
/* strip @ 48px icon (2× bigger), halve stroke width to look the same */
|
||||
.folder-strip-container .folder-svg .folder-front,
|
||||
.folder-strip-container .folder-svg .folder-back,
|
||||
.folder-strip-container .folder-svg .paper-line,
|
||||
.folder-strip-container .folder-svg .paper-ink,
|
||||
.folder-strip-container .folder-svg .lip-highlight { stroke-width: 1.1px; }
|
||||
|
||||
@@ -100,7 +100,7 @@ export function initializeApp() {
|
||||
|
||||
|
||||
// Hook DnD relay from fileList area into upload area
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const fileListArea = document.getElementById('fileList');
|
||||
|
||||
if (fileListArea) {
|
||||
let hoverTimer = null;
|
||||
|
||||
@@ -156,7 +156,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
||||
|
||||
export function buildFileTableHeader(sortOrder) {
|
||||
return `
|
||||
<table class="table">
|
||||
<table class="table filr-table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox-col"><input type="checkbox" id="selectAll"></th>
|
||||
@@ -283,9 +283,9 @@ export function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (!row) return;
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('row-selected');
|
||||
row.classList.add('row-selected', 'selected');
|
||||
} else {
|
||||
row.classList.remove('row-selected');
|
||||
row.classList.remove('row-selected', 'selected');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -367,12 +367,18 @@ async function saveFolderColor(folder, colorHexOrEmpty) {
|
||||
if (!res.ok || data.error) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
// update local map & apply
|
||||
if (data.color) window.folderColorMap[folder] = data.color;
|
||||
else delete window.folderColorMap[folder];
|
||||
applyFolderColorToOption(folder, data.color || '');
|
||||
return data;
|
||||
else delete window.folderColorMap[folder];
|
||||
applyFolderColorToOption(folder, data.color || '');
|
||||
|
||||
// notify other views (fileListView's strip)
|
||||
window.dispatchEvent(new CustomEvent('folderColorChanged', {
|
||||
detail: { folder, color: data.color || '' }
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function openColorFolderModal(folder) {
|
||||
export function openColorFolderModal(folder) {
|
||||
const existing = window.folderColorMap[folder] || '';
|
||||
const defaultHex = existing || '#f6b84e';
|
||||
|
||||
@@ -559,39 +565,86 @@ const _nonEmptyCache = new Map();
|
||||
Folder icon (SVG + fetch + cache)
|
||||
----------------------*/
|
||||
|
||||
// Crisp emoji-like folder (empty / with paper)
|
||||
function folderSVG(kind = 'empty') {
|
||||
// shared by folder tree + folder strip
|
||||
export function folderSVG(kind = 'empty') {
|
||||
const gid = 'g' + Math.random().toString(36).slice(2, 8);
|
||||
|
||||
// tweak these
|
||||
const PAPER_SHIFT_Y = -1.2; // move paper up (negative = up)
|
||||
const INK_SHIFT_Y = -0.8; // extra lift for the blue lines
|
||||
|
||||
return `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<!-- Angled back body -->
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"
|
||||
style="display:block;shape-rendering:geometricPrecision">
|
||||
<defs>
|
||||
<clipPath id="${gid}-clipBack"><path d="M3.5 7.5 H10.5 L12.5 9.5 H20.5
|
||||
C21.6 9.5 22.5 10.4 22.5 11.5 V19.5
|
||||
C22.5 20.6 21.6 21.5 20.5 21.5 H5.5
|
||||
C4.4 21.5 3.5 20.6 3.5 19.5 V9.5
|
||||
C3.5 8.4 4.4 7.5 5.5 7.5 Z"/></clipPath>
|
||||
<clipPath id="${gid}-clipFront"><path d="M2.5 10.5 H11.5 L13.5 8.5 H20.5
|
||||
C21.6 8.5 22.5 9.4 22.5 10.5 V17.5
|
||||
C22.5 18.6 21.6 19.5 20.5 19.5 H4.5
|
||||
C3.4 19.5 2.5 18.6 2.5 17.5 V10.5 Z"/></clipPath>
|
||||
<linearGradient id="${gid}-back" x1="4" y1="20" x2="20" y2="4" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity="0"/>
|
||||
<stop offset=".55" stop-color="#fff" stop-opacity=".10"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="${gid}-front" x1="6" y1="19" x2="19" y2="7" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#000" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity=".06"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- BACK -->
|
||||
<g class="back-group" clip-path="url(#${gid}-clipBack)">
|
||||
<path class="folder-back"
|
||||
d="M3 7.4h7.6l1.6 1.8H20.3c1.1 0 2 .9 2 2v7.6c0 1.1-.9 2-2 2H5
|
||||
c-1.1 0-2-.9-2-2V9.4c0-1.1.9-2 2-2z"/>
|
||||
d="M3.5 7.5 H10.5 L12.5 9.5 H20.5
|
||||
C21.6 9.5 22.5 10.4 22.5 11.5 V19.5
|
||||
C22.5 20.6 21.6 21.5 20.5 21.5 H5.5
|
||||
C4.4 21.5 3.5 20.6 3.5 19.5 V9.5
|
||||
C3.5 8.4 4.4 7.5 5.5 7.5 Z"/>
|
||||
<path d="M3.5 7.5 H10.5 L12.5 9.5 H20.5 V21.5 H3.5 Z"
|
||||
fill="url(#${gid}-back)" pointer-events="none"/>
|
||||
</g>
|
||||
|
||||
${kind === 'paper'
|
||||
? `
|
||||
<!-- Paper raised so it peeks above the lip -->
|
||||
<rect class="paper" x="6.1" y="5.7" width="11.8" height="10.8" rx="1.2"/>
|
||||
<!-- Bigger fold -->
|
||||
<path class="paper-fold" d="M18.0 5.7h-3.2l3.2 3.2z"/>
|
||||
<!-- Content lines -->
|
||||
<path class="paper-line" d="M7.7 8.2h8.3"/>
|
||||
<path class="paper-line" d="M7.7 9.8h7.2"/>
|
||||
<path class="paper-line" d="M7.7 11.3h6.0"/>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
${kind === 'paper' ? `
|
||||
<!-- Move the entire paper block up (keep your existing shift if you use it) -->
|
||||
<g class="paper-group" transform="translate(0, -1.2)">
|
||||
<rect class="paper" x="6.5" y="6.5" width="11" height="10" rx="1"/>
|
||||
|
||||
<!-- Fold aligned to the paper's top-right corner (right edge = 17.5) -->
|
||||
<path class="paper-fold" d="M17.5 6.5 H15.2 L17.5 9.0 Z"/>
|
||||
|
||||
<!-- Front lip (angled) -->
|
||||
<!-- handwriting dashes -->
|
||||
<g transform="translate(0, -2.4)">
|
||||
<path class="paper-ink" d="M9 11.3 H14.2"
|
||||
stroke="#4da3ff" stroke-width=".9" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
paint-order="normal" vector-effect="non-scaling-stroke"/>
|
||||
<path class="paper-ink" d="M9 12.8 H16.4"
|
||||
stroke="#4da3ff" stroke-width=".9" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
paint-order="normal" vector-effect="non-scaling-stroke"/>
|
||||
</g>
|
||||
</g>
|
||||
` : ``}
|
||||
|
||||
<!-- FRONT -->
|
||||
<g class="front-group" clip-path="url(#${gid}-clipFront)">
|
||||
<path class="folder-front"
|
||||
d="M2.3 10.1H10.9l2.0-2.1h7.4c.94 0 1.7.76 1.7 1.7v7.3c0 .94-.76 1.7-1.7 1.7H4
|
||||
c-.94 0-1.7-.76-1.7-1.7v-6.9z"/>
|
||||
d="M2.5 10.5 H11.5 L13.5 8.5 H20.5
|
||||
C21.6 8.5 22.5 9.4 22.5 10.5 V17.5
|
||||
C22.5 18.6 21.6 19.5 20.5 19.5 H4.5
|
||||
C3.4 19.5 2.5 18.6 2.5 17.5 V10.5 Z"/>
|
||||
<path d="M2.5 10.5 H11.5 L13.5 8.5 H20.5 V19.5 H2.5 Z"
|
||||
fill="url(#${gid}-front)" pointer-events="none"/>
|
||||
</g>
|
||||
|
||||
<!-- Subtle highlight along the lip to add depth -->
|
||||
<path class="lip-highlight"
|
||||
d="M3.3 10.2H11.2l1.7-1.8h7.0"
|
||||
/>
|
||||
</svg>`;
|
||||
<!-- Lip highlight -->
|
||||
<path class="lip-highlight" d="M3 10.5 H11.5 L13.5 8.5 H20.3"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const _folderCountCache = new Map();
|
||||
@@ -1253,7 +1306,7 @@ if (submitRename) {
|
||||
}
|
||||
|
||||
// === Move Folder Modal helper (shared by button + context menu) ===
|
||||
function openMoveFolderUI(sourceFolder) {
|
||||
export function openMoveFolderUI(sourceFolder) {
|
||||
const modal = document.getElementById('moveFolderModal');
|
||||
const targetSel = document.getElementById('moveFolderTarget');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user