@@ -26,7 +101,7 @@
@@ -37,12 +112,12 @@
t-ref="fileInput"
multiple="1"
accept="*"
- t-on-change="onChangeFileInput"
+ t-on-change="this.onChangeFileInput"
/>
diff --git a/dms/static/src/js/views/file_kanban_view.esm.js b/dms/static/src/js/views/file_kanban_view.esm.js
index 0fb1f52b0..fdc681559 100644
--- a/dms/static/src/js/views/file_kanban_view.esm.js
+++ b/dms/static/src/js/views/file_kanban_view.esm.js
@@ -17,7 +17,6 @@ import {registry} from "@web/core/registry";
patch(FileKanbanRenderer.prototype, createFileDropZoneExtension());
patch(KanbanController.prototype, createFileUploadExtension());
-FileKanbanRenderer.template = "dms.KanbanRenderer";
export const FileKanbanView = {
...kanbanView,
diff --git a/dms/static/src/scss/dms_ext_palette.scss b/dms/static/src/scss/dms_ext_palette.scss
new file mode 100644
index 000000000..63d5081f4
--- /dev/null
+++ b/dms/static/src/scss/dms_ext_palette.scss
@@ -0,0 +1,158 @@
+// Copyright 2026 ledoent — Don Kendall
+// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+//
+// Per-extension accent palette — single source of truth.
+//
+// Sets `--dms-accent` (CSS custom property) on any card whose `data-ext`
+// attribute matches a known extension family. The variable cascades to
+// descendant elements (spine, ext pill, mime icon) so each surface that
+// adopts the data-ext attribute pattern automatically inherits the right
+// colour without duplicating the map.
+//
+// This file is loaded in BOTH `web.assets_backend` (kanban / list / form)
+// and `web.assets_frontend` (portal) — see __manifest__.py.
+//
+// To support a new card surface, just give it `data-ext` and include its
+// selector in the per-rule selector list below. The default accent for
+// unknown extensions is set on the card class itself in its own SCSS
+// (e.g. `.o_kanban_dms_card { --dms-accent: var(--bs-secondary-color); }`).
+
+// Documents — crimson
+.o_kanban_dms_card[data-ext="pdf"],
+.o_dms_portal_card--file[data-ext="pdf"] {
+ --dms-accent: #d6336c;
+}
+
+// Word-family — blue
+.o_kanban_dms_card[data-ext="doc"],
+.o_kanban_dms_card[data-ext="docx"],
+.o_kanban_dms_card[data-ext="odt"],
+.o_kanban_dms_card[data-ext="rtf"],
+.o_dms_portal_card--file[data-ext="doc"],
+.o_dms_portal_card--file[data-ext="docx"],
+.o_dms_portal_card--file[data-ext="odt"],
+.o_dms_portal_card--file[data-ext="rtf"] {
+ --dms-accent: #1971c2;
+}
+
+// Spreadsheets — green
+.o_kanban_dms_card[data-ext="xls"],
+.o_kanban_dms_card[data-ext="xlsx"],
+.o_kanban_dms_card[data-ext="ods"],
+.o_kanban_dms_card[data-ext="csv"],
+.o_dms_portal_card--file[data-ext="xls"],
+.o_dms_portal_card--file[data-ext="xlsx"],
+.o_dms_portal_card--file[data-ext="ods"],
+.o_dms_portal_card--file[data-ext="csv"] {
+ --dms-accent: #2f9e44;
+}
+
+// Presentations — orange
+.o_kanban_dms_card[data-ext="ppt"],
+.o_kanban_dms_card[data-ext="pptx"],
+.o_kanban_dms_card[data-ext="odp"],
+.o_dms_portal_card--file[data-ext="ppt"],
+.o_dms_portal_card--file[data-ext="pptx"],
+.o_dms_portal_card--file[data-ext="odp"] {
+ --dms-accent: #e8590c;
+}
+
+// Plain text / markdown — slate
+.o_kanban_dms_card[data-ext="txt"],
+.o_kanban_dms_card[data-ext="md"],
+.o_kanban_dms_card[data-ext="rst"],
+.o_dms_portal_card--file[data-ext="txt"],
+.o_dms_portal_card--file[data-ext="md"],
+.o_dms_portal_card--file[data-ext="rst"] {
+ --dms-accent: #495057;
+}
+
+// Code (compiled / scripted) — teal
+.o_kanban_dms_card[data-ext="py"],
+.o_kanban_dms_card[data-ext="js"],
+.o_kanban_dms_card[data-ext="ts"],
+.o_kanban_dms_card[data-ext="java"],
+.o_kanban_dms_card[data-ext="c"],
+.o_kanban_dms_card[data-ext="cc"],
+.o_kanban_dms_card[data-ext="cpp"],
+.o_kanban_dms_card[data-ext="cs"],
+.o_kanban_dms_card[data-ext="h"],
+.o_kanban_dms_card[data-ext="hpp"],
+.o_kanban_dms_card[data-ext="go"],
+.o_kanban_dms_card[data-ext="rs"],
+.o_kanban_dms_card[data-ext="rb"],
+.o_kanban_dms_card[data-ext="php"],
+.o_kanban_dms_card[data-ext="sh"],
+.o_kanban_dms_card[data-ext="bash"],
+.o_kanban_dms_card[data-ext="aj"],
+.o_kanban_dms_card[data-ext="groovy"],
+.o_kanban_dms_card[data-ext="coffee"],
+.o_kanban_dms_card[data-ext="cbl"],
+.o_kanban_dms_card[data-ext="f"],
+.o_kanban_dms_card[data-ext="f90"] {
+ --dms-accent: #1098ad;
+}
+
+// Web markup / styles — purple
+.o_kanban_dms_card[data-ext="html"],
+.o_kanban_dms_card[data-ext="htm"],
+.o_kanban_dms_card[data-ext="xml"],
+.o_kanban_dms_card[data-ext="css"],
+.o_kanban_dms_card[data-ext="scss"],
+.o_kanban_dms_card[data-ext="sass"],
+.o_kanban_dms_card[data-ext="less"],
+.o_kanban_dms_card[data-ext="json"],
+.o_kanban_dms_card[data-ext="yaml"],
+.o_kanban_dms_card[data-ext="yml"],
+.o_kanban_dms_card[data-ext="toml"] {
+ --dms-accent: #7048e8;
+}
+
+// Archives — violet
+.o_kanban_dms_card[data-ext="zip"],
+.o_kanban_dms_card[data-ext="tar"],
+.o_kanban_dms_card[data-ext="gz"],
+.o_kanban_dms_card[data-ext="rar"],
+.o_dms_portal_card--file[data-ext="zip"],
+.o_dms_portal_card--file[data-ext="tar"],
+.o_dms_portal_card--file[data-ext="gz"],
+.o_dms_portal_card--file[data-ext="rar"] {
+ --dms-accent: #5f3dc4;
+}
+
+// Images — same crimson as PDFs (image previews are visually strong on
+// their own; the accent just adds family identity).
+.o_kanban_dms_card[data-ext="jpg"],
+.o_kanban_dms_card[data-ext="jpeg"],
+.o_kanban_dms_card[data-ext="png"],
+.o_kanban_dms_card[data-ext="gif"],
+.o_kanban_dms_card[data-ext="svg"],
+.o_kanban_dms_card[data-ext="webp"],
+.o_dms_portal_card--file[data-ext="jpg"],
+.o_dms_portal_card--file[data-ext="jpeg"],
+.o_dms_portal_card--file[data-ext="png"],
+.o_dms_portal_card--file[data-ext="gif"],
+.o_dms_portal_card--file[data-ext="svg"],
+.o_dms_portal_card--file[data-ext="webp"] {
+ --dms-accent: #d6336c;
+}
+
+// Media (audio + video) — magenta
+.o_kanban_dms_card[data-ext="mp4"],
+.o_kanban_dms_card[data-ext="mov"],
+.o_kanban_dms_card[data-ext="mkv"],
+.o_kanban_dms_card[data-ext="webm"],
+.o_kanban_dms_card[data-ext="mp3"],
+.o_kanban_dms_card[data-ext="wav"],
+.o_kanban_dms_card[data-ext="flac"],
+.o_kanban_dms_card[data-ext="ogg"],
+.o_dms_portal_card--file[data-ext="mp4"],
+.o_dms_portal_card--file[data-ext="mov"],
+.o_dms_portal_card--file[data-ext="mkv"],
+.o_dms_portal_card--file[data-ext="webm"],
+.o_dms_portal_card--file[data-ext="mp3"],
+.o_dms_portal_card--file[data-ext="wav"],
+.o_dms_portal_card--file[data-ext="flac"],
+.o_dms_portal_card--file[data-ext="ogg"] {
+ --dms-accent: #ae3ec9;
+}
diff --git a/dms/static/src/scss/file_kanban.scss b/dms/static/src/scss/file_kanban.scss
new file mode 100644
index 000000000..f23a1ba3a
--- /dev/null
+++ b/dms/static/src/scss/file_kanban.scss
@@ -0,0 +1,438 @@
+// Copyright 2026 ledoent — Don Kendall
+// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+// Denser, mime-aware file kanban card.
+//
+// Density tiers are applied via [data-density] on the kanban view root.
+// Default = "comfortable"; "compact" and "list" are toggled via the bar in
+// the top-right of the kanban area. Selection persists in localStorage.
+
+.mk_file_kanban_view {
+ position: relative;
+
+ // ---- Card chrome (applies to every density) ----
+ .o_kanban_dms_card {
+ // Default accent for unknown extensions / no mimetype info.
+ --dms-accent: var(--bs-secondary-color, #6c757d);
+
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ border-left: 3px solid var(--dms-accent);
+ transition:
+ transform 140ms ease,
+ box-shadow 140ms ease,
+ border-left-width 140ms ease;
+ }
+
+ // Subtle hover lift — cards feel tactile, the accent thickens to draw
+ // the eye to the hovered card without disturbing density.
+ .o_kanban_record:hover .o_kanban_dms_card {
+ transform: translateY(-1px);
+ box-shadow:
+ 0 2px 4px rgba(0, 0, 0, 0.04),
+ 0 4px 12px rgba(0, 0, 0, 0.06);
+ border-left-width: 4px;
+ }
+
+ // Selected: this card is the one open in the side-pane preview. Primary
+ // rail + tint, distinct from the per-extension accent, mirroring the
+ // list view's o_dms_preview_selected_row so the card↔pane mapping stays
+ // legible across densities.
+ .o_kanban_record.o_dms_preview_selected_card .o_kanban_dms_card {
+ --dms-accent: var(--bs-primary, #714b67);
+ border-left-width: 4px;
+ background-color: rgba(113, 75, 103, 0.06);
+ box-shadow: inset 0 0 0 1px rgba(113, 75, 103, 0.18);
+ }
+
+ // Empty many2many tags widget renders a wrapper div with no content;
+ // hide it so cards without tags don't carry empty vertical space.
+ .o_kanban_dms_card__body .o_field_many2many_tags:empty,
+ .o_kanban_dms_card__body .o_field_many2many_tags:has(.o_input_dropdown:only-child) {
+ display: none;
+ }
+
+ // The actual card chrome (thumb / body / meta sub-styles below).
+ .o_kanban_dms_card {
+ &__thumb {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--bs-tertiary-bg, #f4f5f7);
+ border-radius: 4px;
+ overflow: hidden;
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ &__lock {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ padding: 2px 5px;
+ background-color: rgba(255, 255, 255, 0.92);
+ border-radius: 3px;
+ font-size: 0.75rem;
+ color: var(--bs-warning-text-emphasis, #997404);
+ }
+
+ // Extension pill — bottom-right of the thumb. Tinted using the
+ // per-extension accent so the pill reinforces the spine without a
+ // second source of truth for colour. Hidden in compact/list densities.
+ &__ext_pill {
+ position: absolute;
+ bottom: 3px;
+ right: 3px;
+ padding: 0 5px;
+ font-family: var(
+ --bs-font-monospace,
+ ui-monospace,
+ "SFMono-Regular",
+ Menlo,
+ monospace
+ );
+ font-size: 0.62rem;
+ font-weight: 600;
+ line-height: 1.5;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: #fff;
+ background-color: var(--dms-accent, var(--bs-secondary, #6c757d));
+ border-radius: 3px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.12);
+ pointer-events: none;
+ }
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0; // allow text-truncate on the name
+ }
+
+ &__name {
+ display: block;
+ // min-width:0 lets the name shrink (and thus ellipsis-truncate)
+ // when it is a flex child in the compact/list row layouts; a no-op
+ // in the comfortable column layout.
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ // Inline rename input (F3). Sits in the same flow slot as the name
+ // span so the card geometry doesn't shift on dblclick. Bootstrap's
+ // form-control-sm sets the height; we strip the rounded chrome to
+ // keep it visually close to the read-mode label.
+ &__name_input {
+ display: block;
+ width: 100%;
+ font-weight: 600;
+ font-size: inherit;
+ padding: 2px 6px;
+ border: 1px solid var(--bs-primary, #714b67);
+ border-radius: 3px;
+ background: var(--bs-body-bg, #fff);
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(113, 75, 103, 0.2);
+ }
+ }
+
+ // Directory path subtitle — sits directly under the filename so
+ // duplicate filenames across directories are disambiguated at a
+ // glance. Hidden in compact/list densities.
+ &__subtitle {
+ display: block;
+ font-size: 0.72rem;
+ line-height: 1.2;
+ margin-top: -2px;
+ }
+
+ &__meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: auto;
+ padding-top: 4px;
+ border-top: 1px solid
+ var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
+ font-size: 0.75rem;
+ color: var(--bs-secondary-color, #6c757d);
+ }
+
+ &__avatar {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ object-fit: cover;
+ }
+
+ // The inline-rename name is rendered by the dms_file_name view widget,
+ // which Odoo wraps in a . That wrapper would
+ // become the flex child of the card body and, lacking min-width:0,
+ // refuse to shrink — so a long filename overflows into the meta
+ // column in compact/list densities. display:contents removes the
+ // wrapper box so the name/input itself is the flex child, restoring
+ // the truncation behaviour the &__name rules already define.
+ .o_widget_dms_file_name {
+ display: contents;
+ }
+ }
+
+ // Density-aware suppression: the subtitle + ext pill belong to the
+ // comfortable density's "scannable card" composition. Compact + list
+ // densities prioritise row count, so we hide them there.
+ .o_kanban_renderer[data-density="compact"],
+ .o_kanban_renderer[data-density="list"] {
+ .o_kanban_dms_card__subtitle,
+ .o_kanban_dms_card__ext_pill {
+ display: none;
+ }
+ }
+
+ // ---- Comfortable (default): vertical card, denser than Odoo's stock ----
+ // Odoo's `.o_kanban_record` has its own min-height which would let the
+ // thumb stretch to fill (flex-column distributes free space). Override
+ // both the record min-height and pin the thumb to its natural size so
+ // the card sits as tall as the content requires, no more.
+ .o_kanban_renderer[data-density="comfortable"],
+ .o_kanban_renderer:not([data-density]) {
+ .o_kanban_record {
+ min-height: 0;
+ }
+
+ .o_kanban_dms_card {
+ padding: 8px 12px;
+ min-height: 0;
+ gap: 7px;
+
+ // Thumb sized to its content (mime icon or image_128).
+ // Loses the placeholder grey rectangle that dominated the
+ // previous layout and made small icons look stranded.
+ &__thumb {
+ aspect-ratio: auto;
+ flex: 0 0 auto;
+ min-height: 0;
+ padding: 4px 0;
+ background-color: transparent;
+ align-self: flex-start;
+ }
+
+ &__thumb img {
+ width: 48px;
+ height: 48px;
+ }
+
+ // For image files, icon_url returns /web/image/.../image_128 —
+ // those benefit from a wider thumbnail. The dms.mixins.thumbnail
+ // cropping is 128×128 so we can show it square at a larger size.
+ &__thumb img[src*="image_128"] {
+ width: 76px;
+ height: 76px;
+ border-radius: 4px;
+ }
+ }
+ }
+
+ // ---- Compact: row-style with icon + name on a single line ----
+ // Replaces the comfortable card's stacked thumb+body+meta with a
+ // horizontal row inside each card, halving vertical footprint.
+ .o_kanban_renderer[data-density="compact"] {
+ .o_kanban_record {
+ min-height: unset;
+ }
+
+ .o_kanban_dms_card {
+ flex-direction: row;
+ align-items: center;
+ padding: 4px 8px;
+ gap: 10px;
+ min-height: 0;
+
+ &__thumb {
+ flex: 0 0 32px;
+ width: 32px;
+ height: 32px;
+ aspect-ratio: 1;
+ min-height: 0;
+ padding: 0;
+ background-color: transparent;
+ }
+
+ &__thumb img {
+ width: 24px;
+ height: 24px;
+ }
+
+ &__lock {
+ top: 0;
+ right: 0;
+ font-size: 0.65rem;
+ }
+
+ &__body {
+ flex: 1 1 auto;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+ }
+
+ &__name {
+ flex: 1 1 auto;
+ font-size: 0.85rem;
+ }
+
+ &__meta {
+ margin-top: 0;
+ padding-top: 0;
+ border-top: none;
+ flex: 0 0 auto;
+ font-size: 0.7rem;
+ }
+ }
+ }
+
+ // ---- List: horizontal row, icon + meta inline ----
+ .o_kanban_renderer[data-density="list"] {
+ // The renderer IS the flex container — no nested descendant needed.
+ // nowrap is essential: the renderer is a fixed-height flex item (it
+ // fills the space below the toolbar and owns the vertical scroll), so a
+ // column flex-container that still inherited core's `flex-wrap: wrap`
+ // would spill rows sideways into extra columns instead of stacking +
+ // scrolling. Stack in one column and let overflow-y handle the rest.
+ display: flex;
+ flex-direction: column;
+ // !important to beat core's `.flex-wrap` Bootstrap utility class on the
+ // renderer (which is itself !important) — without it the column still
+ // wraps rows sideways into extra columns.
+ flex-wrap: nowrap !important;
+ gap: 2px;
+
+ .o_kanban_record {
+ width: 100% !important;
+ max-width: 100% !important;
+ min-height: unset;
+ margin: 0;
+ // Keep each row's natural height; without this the column's
+ // default flex-shrink squeezes 44 rows into the fixed height
+ // (≈12px each) instead of overflowing into the scroll.
+ flex: 0 0 auto;
+ }
+
+ .o_kanban_dms_card {
+ flex-direction: row;
+ align-items: center;
+ padding: 6px 10px;
+ gap: 12px;
+
+ &__thumb {
+ flex: 0 0 32px;
+ width: 32px;
+ height: 32px;
+ aspect-ratio: 1;
+ min-height: 32px;
+ }
+
+ &__thumb img {
+ width: 24px;
+ height: 24px;
+ }
+
+ &__body {
+ flex: 1 1 auto;
+ flex-direction: row;
+ align-items: center;
+ gap: 12px;
+ }
+
+ &__meta {
+ margin-top: 0;
+ padding-top: 0;
+ border-top: none;
+ flex: 0 0 auto;
+ }
+ }
+ }
+
+ // ---- Animated dropzone (drag-over overlay) ----
+ //
+ // Replaces the default static cloud icon with a marching-dashed-border
+ // overlay + pulsing icon + title + hint. Triggered by the renderer's
+ // existing `dragState.showDragZone` state — no JS change.
+ .o_dms_dropzone {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ background:
+ linear-gradient(rgba(113, 75, 103, 0.06), rgba(113, 75, 103, 0.1)),
+ repeating-linear-gradient(
+ 45deg,
+ transparent,
+ transparent 8px,
+ rgba(113, 75, 103, 0.06) 8px,
+ rgba(113, 75, 103, 0.06) 16px
+ );
+ border-radius: 8px;
+ outline: 3px dashed var(--bs-primary, #714b67);
+ outline-offset: -8px;
+ animation: o_dms_dropzone_march 1s linear infinite;
+ z-index: 10;
+
+ &__icon {
+ width: 96px;
+ height: 96px;
+ border-radius: 50%;
+ background: rgba(113, 75, 103, 0.12);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: o_dms_dropzone_pulse 1.5s ease-in-out infinite;
+
+ i.fa {
+ font-size: 3rem;
+ color: var(--bs-primary, #714b67);
+ }
+ }
+
+ &__title {
+ font-size: 1.2rem;
+ font-weight: 600;
+ color: var(--bs-primary, #714b67);
+ }
+
+ &__hint {
+ font-size: 0.85rem;
+ color: var(--bs-secondary-color, #6c757d);
+ }
+ }
+}
+
+@keyframes o_dms_dropzone_march {
+ from {
+ outline-offset: -8px;
+ }
+ to {
+ outline-offset: -16px;
+ }
+}
+
+@keyframes o_dms_dropzone_pulse {
+ 0%,
+ 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.08);
+ }
+}
diff --git a/dms/views/dms_file.xml b/dms/views/dms_file.xml
index ed44ad578..30180f77a 100644
--- a/dms/views/dms_file.xml
+++ b/dms/views/dms_file.xml
@@ -93,15 +93,16 @@
js_class="file_kanban"
class="mk_file_kanban_view o_kanban_small_column o_kanban_mobile"
banner_route="/onboarding/document_onboarding_file"
- highlight_color="color"
>
+
+
@@ -211,53 +212,72 @@