# File row

One chosen file: its name, size and state, its progress while it uploads, and what can be done with it.

Block: `smbk-file-row`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `status` | `selected`, `uploading`, `complete`, `failed`, `rejected` | `selected` | Selected is chosen and waiting; uploading shows the spinner and the progress bar; complete has gone through; failed did not, and can be tried again; rejected was not added, and says why. |

Parts: `smbk-file-row__top`, `smbk-file-row__icon`, `smbk-file-row__text`, `smbk-file-row__name`, `smbk-file-row__stem`, `smbk-file-row__extension`, `smbk-file-row__meta`, `smbk-file-row__size`, `smbk-file-row__separator`, `smbk-file-row__state`, `smbk-file-row__state-icon`, `smbk-file-row__message`, `smbk-file-row__actions`, `smbk-file-row__progress`

## Markup

```html
<div class="smbk-file-row smbk-file-row--status-selected">
  <div class="smbk-file-row__top">
    <i
      class="ph ph-file-pdf smbk-icon smbk-icon--size-md smbk-file-row__icon"
      aria-hidden="true"
    ></i>
    <div class="smbk-file-row__text">
      <p class="smbk-file-row__name" dir="auto">
        <span class="smbk-file-row__stem">quarterly-report</span>
        <span class="smbk-file-row__extension">.pdf</span>
      </p>
      <p class="smbk-file-row__meta">
        <span class="smbk-file-row__size">2.4 MB</span>
      </p>
    </div>
    <div class="smbk-file-row__actions">
      <button
        type="button"
        class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-sm smbk-btn--icon-only"
        aria-label="Remove quarterly-report.pdf"
      >
        <i class="ph ph-x smbk-icon smbk-icon--size-md smbk-btn__icon" aria-hidden="true"></i>
      </button>
    </div>
  </div>
</div>
```

## React

```tsx
import { FileRow } from './design-system/components/file-row/file-row.js';

<FileRow status="selected" name="quarterly-report.pdf" size={2400000} loaded={1100000} onRemove={() => {}} onCancel={() => {}} onRetry={() => {}} onDelete={() => {}} />
```

## Stylesheet

```css
/* smbk-file-row — File row. Figma "File row" 7110:252 (Status: Selected, Uploading, Complete, Failed, Rejected).
 * One chosen file: a card that stacks a top row over, while it uploads, the progress bar. The top row holds the
 * file's icon at the 2xl icon size, then its name over a line with the size, a dot and the state, then its
 * actions, the button's small ghost icon-only form, at the top end. The name is two texts, so a long one is cut
 * short with an ellipsis and the extension stays whole. The state is an icon (or the spinner's ring) and words,
 * so it never rests on color alone; a failed or refused file shows only the reason, which wraps beside its icon.
 * The edge is drawn inside, so the paddings give its width back. */

.smbk-file-row {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-md);
  min-width: 0;
  padding: calc(var(--smbk-space-inset-lg) - var(--smbk-border-width-hairline));
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
  border-radius: var(--smbk-radius-container-sm);
  background: var(--smbk-color-surface-default);
  color: var(--smbk-color-text-default);
}

.smbk-file-row__top {
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-lg);
  min-width: 0;
}

/* The file's type, as an icon chosen from its extension: decoration, since the name says the same. */
.smbk-file-row__icon {
  flex: none;
  font-size: var(--smbk-size-icon-2xl);
  color: var(--smbk-color-icon-subtle);
}

.smbk-file-row__text {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--smbk-space-stack-xs);
  min-width: 0;
}

.smbk-file-row__name {
  display: flex;
  min-width: 0;
  margin: 0;
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-body-sm-medium-font-family);
  font-size: var(--smbk-typography-body-sm-medium-font-size);
  font-weight: var(--smbk-typography-body-sm-medium-font-weight);
  line-height: var(--smbk-typography-body-sm-medium-line-height);
  letter-spacing: var(--smbk-typography-body-sm-medium-letter-spacing);
}

/* The name before its extension shrinks first, cut short with an ellipsis; the extension keeps its width. */
.smbk-file-row__stem {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.smbk-file-row__extension {
  flex: none;
  white-space: nowrap;
}

/* The line under the name: the size, a dot, then the state. It never wraps between them; only a long reason
   wraps, beside its own icon. */
.smbk-file-row__meta {
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-sm);
  min-width: 0;
  margin: 0;
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
}

.smbk-file-row__size,
.smbk-file-row__separator {
  flex: none;
  white-space: nowrap;
}

/* Figures of one width, so the size counting up while a file uploads ("1.1 of 2.4 MB") never moves the state after it. */
.smbk-file-row__size {
  font-variant-numeric: tabular-nums;
}

.smbk-file-row__state {
  display: flex;
  flex: 1;
  align-items: flex-start;
  gap: var(--smbk-space-inline-xs);
  min-width: 0;
}

/* The state's icon, and the spinner's ring while uploading, sit in a box one line tall, on the first line's middle. */
.smbk-file-row__state-icon {
  flex: none;
  height: calc(var(--smbk-typography-body-xs-font-size) * var(--smbk-typography-body-xs-line-height));
  font-size: var(--smbk-size-icon-sm);
}

.smbk-file-row__message {
  min-width: 0;
}

.smbk-file-row--status-complete .smbk-file-row__message {
  color: var(--smbk-color-feedback-success-text-on-subtle);
}

.smbk-file-row--status-complete .smbk-file-row__state-icon {
  color: var(--smbk-color-feedback-success-icon);
}

.smbk-file-row--status-failed .smbk-file-row__message,
.smbk-file-row--status-rejected .smbk-file-row__message {
  color: var(--smbk-color-feedback-danger-text-on-subtle);
}

.smbk-file-row--status-failed .smbk-file-row__state-icon,
.smbk-file-row--status-rejected .smbk-file-row__state-icon {
  color: var(--smbk-color-feedback-danger-icon);
}

.smbk-file-row__actions {
  display: flex;
  flex: none;
  gap: var(--smbk-space-inline-xs);
}

/* While uploading: the progress bar's small size, with no label, the row's full width. */
.smbk-file-row__progress {
  width: 100%;
}

/* ---- touch: the actions stop their press areas halfway to each other ----
   Every button takes an invisible press area on a touch screen; the row's actions stand closer than that, so between
   two of them each area reaches only halfway into their gap. Nothing drawn moves. */

@media (pointer: coarse) {
  .smbk-file-row__actions > * + * .smbk-btn::after,
  .smbk-file-row__actions > .smbk-btn + .smbk-btn::after,
  .smbk-file-row__actions > * + .smbk-btn::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
  }

  .smbk-file-row__actions > *:has(+ *) .smbk-btn::after,
  .smbk-file-row__actions > .smbk-btn:has(+ *)::after {
    inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "stack": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things stacked one above another, medium."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things stacked one above another, extra small."
      }
    },
    "inset": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      }
    },
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "color": {
    "border": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "icon": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Quiet icons."
      }
    },
    "feedback": {
      "success": {
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.green.700}}",
          "$description": "Success messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.green.600}}",
          "$description": "The success tone’s own icon (a check), so the tone is never shown by colour alone."
        }
      },
      "danger": {
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.red.700}}",
          "$description": "Danger messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    }
  },
  "radius": {
    "container": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of cards, panels and dialogs, small."
      }
    }
  },
  "size": {
    "icon": {
      "2xl": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Icon box, 2× large."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "typography": {
    "body-sm-medium": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 500,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Body SM one weight step bolder, for the current item, so the current item is not shown by colour alone."
    },
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

One chosen file, shown as a card: its type, its name, its size and its state, its progress while it uploads, and the buttons for what can be done with it. A file upload draws one for each file chosen; a list of attachments can use it on its own.

## Anatomy

- The block, a card with a subtle edge: the top part, then, while the file uploads, the progress part.
- The top part, a row: the icon part, the text part, then the actions part at the top end.
- The icon part: the file's type, such as a document, a spreadsheet or a picture, picked from its extension or its media type, at the 2xl icon size.
- The text part: the name part over the meta part.
- The name part: the stem part, the name before its extension, then the extension part. A long name is cut short with an ellipsis in the stem, and the extension stays whole.
- The meta part, one line: the size part, the separator part (a dot), then the state part. The size shows while the file is chosen, uploading or uploaded; while it uploads it says how much has gone ("1.1 of 2.4 MB").
- The state part: the state-icon part (the spinner's subtle ring while uploading, a tick once uploaded, a crossed circle when it failed, a warning sign when it was not added) and the message part, the words. A failed or refused file shows only its icon and reason, which wraps beside its icon.
- The actions part: the button's small ghost icon-only form, named with the file: Remove for a file chosen, failed or not added; Cancel while it uploads; Try again then Remove when it failed; Delete once uploaded.
- The progress part: the progress bar's small size with no label, the row's full width, named with the file.
- The status is a modifier: `--status-selected`, `--status-uploading`, `--status-complete`, `--status-failed`, `--status-rejected`.

## When to use

- Each file chosen in a file upload, which draws them for you.
- A list of files already attached, such as the attachments of a message.

## When not to use

- A file to open or download in a list of documents: use a [list](list.md), whose rows are links.
- A folder of files to browse: use a [table](table.md) or a list.

## Behavior

The row is markup and words only: it uploads nothing, and the page sets its status and its progress. Each button is drawn only when its handler is given, so a row offers only what the page can do. While the file uploads, the bar fills from `loaded` and `size`, or from `progress`; without either it shows its unknown state. The spinner's ring turns while the file uploads, and stands still under reduced motion. Sizes are written in English, or in the language set with `locale`, so a page prepared on a server shows the same sizes once its script runs.

## Accessibility

### What it does for you

- The file's icon is decoration, hidden from assistive technology: the name says the same.
- Each button is named with the file, such as "Remove quarterly-report.pdf".
- The state is an icon and words, never color alone. A failed or refused file says why in words.
- The progress bar is named with the file.
- The name is written as text, never as markup, and follows its own direction, so a name in a right-to-left script reads the right way.
- A row alone announces nothing. In a file upload, its changes of status are read out for it.

### What you must do

- Use `removeLabel`, `cancelLabel`, `retryLabel` and `deleteLabel` to change the buttons' words for another language.
- Say why a file was refused, in its message.
- On a page that uses rows on their own, announce their changes yourself, once each, not every percent.

### How to test

- With a screen reader, each button is read with the file's name, and the progress bar is read by the file.
- Tab through the row: each button takes focus, and Enter or Space presses it.
- With the colors removed, the icon and words still say whether the file is uploading, done, failed or refused.
- With text at 200% and the page 320 pixels wide, a long name is cut short with an ellipsis, its extension stays whole, and the buttons stay in view.
- In a forced-color mode, the icon and the buttons are still visible.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the next button in the row. |
| Enter or Space | Presses the focused button: remove, cancel, try again or delete. |

## Usage rules

### Do

- Say why a file was not added, in its message: "Not added: it must be a PDF."
- Give only the handlers the page can carry out.

### Don't

- Show a file as uploaded before the server has taken it.
- Read out every percent of an upload.
