Menu

Change language

GitHub repo

File Manager

blockmedia

Storage/files page with breadcrumbs, search, grid/list view toggle, upload action, and a storage meter over hardcoded folders and files.

Installation

$npx uipkge-ng@latest add file-manager
Manifest:https://uipkge.dev/r/angular/file-manager.jsonInstalls to:src/app/components/blocks/file-manager/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

FolderItem
interface FolderItem {
  id: string
  name: string
  count: number
}
FileItem
interface FileItem {
  id: string
  name: string
  kind: FileKind
  size: string
  modified: string
}

Files installed (2)

  • src/app/components/blocks/file-manager/file-manager.component.ts12.4 kB
    // Storage/files page with breadcrumbs, search, grid/list view toggle, upload action,
    // and a storage meter over hardcoded folders and files. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import {
      Download,
      FileArchive,
      FileImage,
      FileSpreadsheet,
      FileText,
      FileVideo,
      Folder,
      LayoutGrid,
      List,
      LucideAngularModule,
      MoreVertical,
      Pencil,
      Search,
      Share2,
      Trash2,
      Upload,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import {
      UiBreadcrumbComponent,
      UiBreadcrumbItemComponent,
      UiBreadcrumbLinkComponent,
      UiBreadcrumbListComponent,
      UiBreadcrumbPageComponent,
      UiBreadcrumbSeparatorComponent,
    } from '@/ui/breadcrumb/breadcrumb.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import { UiToggleGroupComponent, UiToggleGroupItemComponent } from '@/ui/toggle-group/toggle-group.component'
    
    export type FileKind = 'pdf' | 'image' | 'sheet' | 'video' | 'archive'
    
    export interface FolderItem {
      id: string
      name: string
      count: number
    }
    
    export interface FileItem {
      id: string
      name: string
      kind: FileKind
      size: string
      modified: string
    }
    
    export type FileManagerRow = (FolderItem & { isFolder: true }) | (FileItem & { isFolder: false })
    
    const FOLDERS: FolderItem[] = [
      { id: 'folder-design-assets', name: 'Design Assets', count: 24 },
      { id: 'folder-client-files', name: 'Client Files', count: 12 },
      { id: 'folder-brand-kit', name: 'Brand Kit', count: 8 },
      { id: 'folder-archive', name: 'Archive', count: 56 },
    ]
    
    const FILES: FileItem[] = [
      { id: 'file-brand-guidelines', name: 'brand-guidelines.pdf', kind: 'pdf', size: '4.2 MB', modified: 'Mar 14, 2026' },
      { id: 'file-homepage-hero', name: 'homepage-hero.png', kind: 'image', size: '2.8 MB', modified: 'Mar 12, 2026' },
      { id: 'file-logo-final', name: 'logo-final.svg', kind: 'image', size: '48 KB', modified: 'Mar 10, 2026' },
      { id: 'file-pricing-model', name: 'pricing-model.xlsx', kind: 'sheet', size: '316 KB', modified: 'Mar 8, 2026' },
      { id: 'file-launch-video', name: 'launch-video.mp4', kind: 'video', size: '184 MB', modified: 'Mar 5, 2026' },
      { id: 'file-app-iconset', name: 'app-iconset.zip', kind: 'archive', size: '12.4 MB', modified: 'Feb 27, 2026' },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-file-manager, [ui-file-manager]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBreadcrumbComponent,
        UiBreadcrumbItemComponent,
        UiBreadcrumbLinkComponent,
        UiBreadcrumbListComponent,
        UiBreadcrumbPageComponent,
        UiBreadcrumbSeparatorComponent,
        UiButtonComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiInputComponent,
        UiProgressComponent,
        UiToggleGroupComponent,
        UiToggleGroupItemComponent,
      ],
      template: `
        <div data-slot="file-manager" [class]="rootClass">
          <div class="bg-card text-card-foreground rounded-xl border shadow-xs">
            <div class="flex flex-wrap items-center gap-3 border-b p-4">
              <ui-breadcrumb>
                <ui-breadcrumb-list>
                  <ui-breadcrumb-item>
                    <ui-breadcrumb-link href="#" class="inline-flex min-h-6 items-center">Home</ui-breadcrumb-link>
                  </ui-breadcrumb-item>
                  <ui-breadcrumb-separator />
                  <ui-breadcrumb-item>
                    <ui-breadcrumb-link href="#" class="inline-flex min-h-6 items-center">Documents</ui-breadcrumb-link>
                  </ui-breadcrumb-item>
                  <ui-breadcrumb-separator />
                  <ui-breadcrumb-item>
                    <ui-breadcrumb-page>Design</ui-breadcrumb-page>
                  </ui-breadcrumb-item>
                </ui-breadcrumb-list>
              </ui-breadcrumb>
    
              <div class="relative ml-auto w-full max-w-56 sm:w-auto">
                <lucide-icon
                  [img]="Search"
                  class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2"
                />
                <ui-input type="search" placeholder="Search files…" class="pl-8" />
              </div>
    
              <ui-toggle-group
                type="single"
                size="sm"
                [value]="view()"
                (valueChange)="setView($event)"
                class="bg-muted items-center gap-0.5 rounded-md p-0.5"
                aria-label="View mode"
              >
                <button
                  ui-toggle-group-item
                  value="grid"
                  class="data-[state=on]:bg-background size-7 rounded-sm p-0 data-[state=on]:shadow-sm"
                  title="Grid view"
                  aria-label="Grid view"
                >
                  <lucide-icon [img]="LayoutGrid" class="size-4" />
                </button>
                <button
                  ui-toggle-group-item
                  value="list"
                  class="data-[state=on]:bg-background size-7 rounded-sm p-0 data-[state=on]:shadow-sm"
                  title="List view"
                  aria-label="List view"
                >
                  <lucide-icon [img]="List" class="size-4" />
                </button>
              </ui-toggle-group>
    
              <button ui-button size="sm">
                <lucide-icon [img]="Upload" class="size-4" />
                Upload
              </button>
            </div>
    
            <div class="flex flex-wrap items-center gap-x-4 gap-y-2 border-b px-4 py-3">
              <div ui-progress [value]="36.4" class="h-1.5 min-w-40 flex-1 sm:max-w-xs" aria-label="Storage used"></div>
              <p class="text-muted-foreground text-xs">18.2 GB of 50 GB used</p>
            </div>
    
            @if (view() === 'grid') {
              <div class="grid grid-cols-2 gap-3 p-4 sm:grid-cols-3 lg:grid-cols-4">
                @for (folder of folders; track folder.id) {
                  <button
                    type="button"
                    class="hover:bg-muted/50 focus-visible:ring-ring flex flex-col items-start gap-3 rounded-lg border p-3 text-left shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
                  >
                    <span class="bg-warning/10 text-warning grid size-9 place-items-center rounded-md">
                      <lucide-icon [img]="Folder" class="size-5" />
                    </span>
                    <span class="w-full min-w-0">
                      <span class="block truncate text-sm font-medium">{{ folder.name }}</span>
                      <span class="text-muted-foreground block text-xs">{{ folder.count }} items</span>
                    </span>
                  </button>
                }
    
                @for (file of files; track file.id) {
                  <button
                    type="button"
                    class="hover:bg-muted/50 focus-visible:ring-ring group flex flex-col items-start gap-3 rounded-lg border p-3 text-left shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
                  >
                    <span
                      class="bg-muted text-muted-foreground group-hover:bg-background grid size-9 place-items-center rounded-md transition-colors"
                    >
                      <lucide-icon [img]="kindIcon(file.kind)" class="size-5" />
                    </span>
                    <span class="min-w-0">
                      <span class="block truncate text-sm font-medium">{{ file.name }}</span>
                      <span class="text-muted-foreground block text-xs">{{ file.size }} · {{ file.modified }}</span>
                    </span>
                  </button>
                }
              </div>
            } @else {
              <div role="table" aria-label="Files and folders">
                <div role="row" class="text-muted-foreground hidden border-b px-4 py-2 text-xs sm:flex">
                  <span role="columnheader" class="flex-1 pl-9">Name</span>
                  <span role="columnheader" class="w-20 text-right">Size</span>
                  <span role="columnheader" class="w-28 text-right">Modified</span>
                  <span class="w-9"></span>
                </div>
                <ul role="rowgroup" class="divide-y">
                  @for (item of rows(); track item.id) {
                    <li role="row" class="hover:bg-muted/50 flex items-center gap-3 px-4 py-2.5 transition-colors">
                      <span
                        class="grid size-6 shrink-0 place-items-center rounded-md"
                        [class]="item.isFolder ? 'bg-warning/10 text-warning' : 'bg-muted text-muted-foreground'"
                      >
                        @if (item.isFolder) {
                          <lucide-icon [img]="Folder" class="size-4" />
                        } @else {
                          <lucide-icon [img]="kindIcon(item.kind)" class="size-4" />
                        }
                      </span>
                      <span role="cell" class="min-w-0 flex-1 truncate text-sm font-medium">{{ item.name }}</span>
                      <span role="cell" class="text-muted-foreground hidden w-20 shrink-0 text-right text-xs sm:block">
                        {{ item.isFolder ? item.count + ' items' : item.size }}
                      </span>
                      <span role="cell" class="text-muted-foreground hidden w-28 shrink-0 text-right text-xs md:block">
                        {{ item.isFolder ? '—' : item.modified }}
                      </span>
                      <ui-dropdown-menu>
                        <button
                          ui-button
                          ui-dropdown-menu-trigger
                          variant="ghost"
                          size="icon-sm"
                          class="shrink-0"
                          [attr.aria-label]="'Actions for ' + item.name"
                        >
                          <lucide-icon [img]="MoreVertical" class="size-4" />
                        </button>
                        <ui-dropdown-menu-content align="end">
                          <ui-dropdown-menu-item>
                            <lucide-icon [img]="Download" class="size-4" />
                            Download
                          </ui-dropdown-menu-item>
                          <ui-dropdown-menu-item>
                            <lucide-icon [img]="Pencil" class="size-4" />
                            Rename
                          </ui-dropdown-menu-item>
                          <ui-dropdown-menu-item>
                            <lucide-icon [img]="Share2" class="size-4" />
                            Share link
                          </ui-dropdown-menu-item>
                          <ui-dropdown-menu-separator />
                          <ui-dropdown-menu-item variant="destructive">
                            <lucide-icon [img]="Trash2" class="size-4" />
                            Delete
                          </ui-dropdown-menu-item>
                        </ui-dropdown-menu-content>
                      </ui-dropdown-menu>
                    </li>
                  }
                </ul>
              </div>
            }
          </div>
        </div>
      `,
    })
    export class UiFileManagerComponent {
      protected readonly Download = Download
      protected readonly Folder = Folder
      protected readonly LayoutGrid = LayoutGrid
      protected readonly List = List
      protected readonly MoreVertical = MoreVertical
      protected readonly Pencil = Pencil
      protected readonly Search = Search
      protected readonly Share2 = Share2
      protected readonly Trash2 = Trash2
      protected readonly Upload = Upload
    
      @Input() set variant(v: 'grid' | 'list') {
        if (v === 'grid' || v === 'list') this.view.set(v)
      }
      @Input('class') className?: string
    
      readonly view = signal<'grid' | 'list'>('grid')
    
      protected readonly folders: FolderItem[] = FOLDERS
      protected readonly files: FileItem[] = FILES
    
      get rootClass(): string {
        return cn('w-full', this.className)
      }
    
      rows(): FileManagerRow[] {
        return [
          ...this.folders.map((f) => ({ ...f, isFolder: true as const })),
          ...this.files.map((f) => ({ ...f, isFolder: false as const })),
        ]
      }
    
      kindIcon(kind: FileKind): typeof FileText {
        switch (kind) {
          case 'image':
            return FileImage
          case 'sheet':
            return FileSpreadsheet
          case 'video':
            return FileVideo
          case 'archive':
            return FileArchive
          default:
            return FileText
        }
      }
    
      setView(v: unknown): void {
        if (v === 'grid' || v === 'list') this.view.set(v)
      }
    }
    
  • src/app/components/blocks/file-manager/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/file-manager.json