Files
Voiced/src/styles/carbon.scss
T
xavierk 4cf019988d Add backup/restore, daily auto-backup and history CSV/JSON export (Phase E3)
- create_backup writes a zip (manifest with sha256 per file, a VACUUM INTO database snapshot, assets, archive, fonts) atomically. restore_backup validates the manifest, rejects tampered files, path traversal, symlinks and newer schemas, checks the staged DB with integrity_check, then stages a pending restore. The swap runs on the next start before the database opens, moves the current data to backups/pre-restore-<ts>/ and rolls back on any failure.
- Daily automatic backup (migration M9 auto_backup, default on) keeps the newest 14 and never blocks startup.
- Settings Data tab: back up now, restore with confirmation and restart, auto-backup toggle, last backup time.
- History exports the filtered rows as CSV (UTF-8 BOM, CRLF, formula-injection guard) or JSON (voiced.history.v1) with CA-friendly tax columns from a new list_invoice_ledger command.
- Rust round-trip, rejection, swap-rollback and retention tests; vitest for the export and backup helpers.

Adds the zip 8.6.0 and typed-path 0.12.3 crates (pure Rust); package builds must vendor them. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
2026-10-04 18:13:57 +05:30

701 lines
12 KiB
SCSS

// Voiced — Carbon Design System, compiled with self-hosted IBM Plex for offline use.
// Font-face emission from Carbon is disabled so we can serve the fonts ourselves
// instead of pulling them from IBM's CDN (the app must work with no network).
@use '@carbon/react' with (
$css--font-face: false,
$use-akamai-cdn: false
);
/* ---------- IBM Plex (bundled in /public/fonts) ---------- */
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/fonts/IBMPlexSans-Light.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/fonts/IBMPlexSans-LightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/IBMPlexSans-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/fonts/IBMPlexSans-Italic.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/IBMPlexSans-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('/fonts/IBMPlexSans-MediumItalic.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/IBMPlexSans-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/fonts/IBMPlexSans-SemiBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/IBMPlexSans-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/fonts/IBMPlexSans-BoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/IBMPlexMono-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/IBMPlexMono-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/IBMPlexMono-SemiBold.woff2') format('woff2');
}
/* ---------- App shell ---------- */
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background-color: var(--cds-background);
color: var(--cds-text-primary);
}
.voiced-brand {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}
.voiced-brand__logo {
height: 1.25rem;
width: auto;
display: block;
}
.voiced-brand__divider {
width: 1px;
height: 1.25rem;
background: var(--cds-border-subtle-01);
}
.voiced-brand__product {
font-weight: 600;
letter-spacing: 0.02em;
white-space: nowrap;
}
.voiced-header-action {
display: flex;
align-items: center;
gap: 0.5rem;
}
// Views stay mounted while hidden so their state survives navigation.
[hidden] {
display: none !important;
}
.voiced-toasts {
position: fixed;
top: 3.5rem; /* below the 3rem Carbon header */
right: 1rem;
z-index: 9100;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: calc(100vw - 2rem);
pointer-events: none;
}
.voiced-toasts .cds--toast-notification {
pointer-events: auto;
margin: 0;
}
.voiced-page {
padding: 2rem;
max-width: 1600px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.voiced-page__title {
margin-bottom: 0.25rem;
}
.voiced-page__subtitle {
color: var(--cds-text-secondary);
margin-bottom: 2rem;
}
.voiced-stack > * + * {
margin-top: 1.5rem;
}
.voiced-row-gap {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
}
.voiced-muted {
color: var(--cds-text-secondary);
}
.voiced-empty {
padding: 3rem 1rem;
text-align: center;
color: var(--cds-text-secondary);
}
/* ---------- PDF preview column ---------- */
.voiced-page-setup {
margin-bottom: 1rem;
}
.voiced-page-setup__row {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 1rem;
}
.voiced-page-setup__extra {
margin-top: 0.75rem;
}
.voiced-page-setup__field {
min-width: 11rem;
flex: 1 1 11rem;
}
.voiced-page-setup__field .cds--label {
display: block;
margin-bottom: 0.5rem;
}
.voiced-page-setup__footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
margin-top: 0.75rem;
}
.voiced-page-setup__note {
font-size: 0.75rem;
flex: 1 1 12rem;
}
.voiced-template-picker__note {
margin-bottom: 1rem;
font-size: 0.875rem;
}
.voiced-template-picker__status {
flex: 1 1 auto;
align-self: center;
padding-left: 1rem;
font-size: 0.75rem;
}
.voiced-template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
gap: 1rem;
}
.voiced-template-tile {
display: flex;
flex-direction: column;
gap: 0.75rem;
height: 100%;
min-height: 0;
box-sizing: border-box;
}
.voiced-template-tile--selected {
outline: 2px solid var(--cds-focus);
outline-offset: -2px;
}
.voiced-template-tile__thumb {
display: block;
width: 100%;
aspect-ratio: 420 / 594;
object-fit: cover;
border: 1px solid var(--cds-border-subtle);
background: var(--cds-layer);
}
.voiced-template-tile__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.voiced-template-tile__name {
font-weight: 600;
}
.voiced-template-tile__check {
flex: none;
fill: var(--cds-support-success);
}
.voiced-template-tile__tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.voiced-template-tile__tags .cds--tag {
margin: 0;
}
.voiced-pdf-preview {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
}
.voiced-pdf-preview__status {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.25rem 1rem;
min-height: 2rem;
}
.voiced-pdf-preview__chips,
.voiced-pdf-preview__actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.voiced-pdf-preview__empty {
display: flex;
align-items: center;
justify-content: center;
height: 12rem;
background: var(--cds-layer-accent);
color: var(--cds-text-secondary);
}
.voiced-pdf-viewer {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
}
.voiced-pdf-viewer__bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
}
.voiced-pdf-viewer__zoom {
flex: 0 1 18rem;
}
.voiced-pdf-viewer__page {
font-size: 0.875rem;
color: var(--cds-text-secondary);
white-space: nowrap;
}
.voiced-pdf-preview__scroll {
height: calc(100vh - 25rem);
min-height: 20rem;
overflow: auto;
background: var(--cds-layer-accent);
}
.voiced-pdf-viewer--focus .voiced-pdf-preview__scroll {
height: calc(100vh - 12rem);
}
/* The wrapper is as wide as the widest page so a zoomed page scrolls instead of being clipped. */
.voiced-pdf-preview__pages {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
min-width: 100%;
width: max-content;
padding: 16px;
}
.voiced-pdf-preview__page {
flex: none;
background: #ffffff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
.voiced-pdf-preview__canvas {
display: block;
width: 100%;
height: 100%;
}
.voiced-pdf-focus .cds--modal-container {
width: 96vw;
max-width: 96vw;
height: 94vh;
max-height: 94vh;
}
.voiced-pdf-issues {
width: 22rem;
max-width: 80vw;
max-height: 20rem;
overflow: auto;
padding: 0.75rem 1rem;
}
.voiced-pdf-issues__list {
list-style: none;
margin: 0;
padding: 0;
}
.voiced-pdf-issues__item + .voiced-pdf-issues__item {
margin-top: 0.75rem;
}
.voiced-pdf-issues__head {
display: flex;
align-items: center;
gap: 0.5rem;
}
.voiced-pdf-issues__message {
margin: 0.25rem 0 0;
font-size: 0.875rem;
}
.voiced-tag--warn.cds--tag {
background-color: var(--cds-support-warning);
color: #161616;
}
/* ---------- export feedback ---------- */
.voiced-notice-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.voiced-export-split {
display: inline-flex;
align-items: stretch;
}
.voiced-diagnostics__check {
padding: 0.5rem 0;
border-bottom: 1px solid var(--cds-border-subtle-01);
}
.voiced-diagnostics__head {
display: flex;
align-items: center;
gap: 0.5rem;
}
.voiced-diagnostics__label {
flex: 1 1 auto;
min-width: 0;
}
.voiced-diagnostics__ms {
color: var(--cds-text-secondary);
font-size: 0.75rem;
white-space: nowrap;
}
.voiced-diagnostics__detail {
margin: 0.25rem 0 0;
color: var(--cds-text-secondary);
font-size: 0.75rem;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* ---------- Logo preview (Settings -> Branding) ---------- */
.voiced-logo-preview {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.voiced-logo-preview__item {
margin: 0;
width: 14rem;
}
.voiced-logo-preview__surface {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
height: 6rem;
padding: 1rem;
border: 1px solid var(--cds-border-subtle-01);
overflow: hidden;
img {
display: block;
max-width: 100%;
max-height: 4rem;
object-fit: contain;
}
}
.voiced-logo-preview__surface--light {
background: #ffffff;
}
.voiced-logo-preview__surface--dark {
background: #161616;
}
.voiced-logo-preview__chip {
display: flex;
box-sizing: border-box;
max-width: 100%;
padding: 0.5rem 0.75rem;
background: #ffffff;
border-radius: 0.5rem;
img {
max-height: 3rem;
}
}
.voiced-logo-preview__item figcaption {
margin-top: 0.5rem;
font-size: 0.75rem;
}
/* ---------- Settings: Data tab ---------- */
.voiced-section-title {
font-size: 1.25rem;
font-weight: 400;
margin-bottom: 0.5rem;
}
.voiced-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
.voiced-list {
margin-left: 1.25rem;
list-style: disc;
}
/* ---------- Invoice history and detail ---------- */
.voiced-num {
text-align: right;
}
.voiced-history__fy {
font-size: 1.25rem;
font-weight: 400;
margin-bottom: 0.5rem;
}
.voiced-history__tiles,
.voiced-detail__tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.voiced-history__amount,
.voiced-detail__amount {
font-size: 1.75rem;
font-weight: 400;
}
.voiced-history__tile--overdue {
border-left: 4px solid var(--cds-support-error);
}
.voiced-history__filters {
display: grid;
grid-template-columns: minmax(14rem, 2fr) repeat(4, minmax(9rem, 1fr)) auto;
gap: 1rem;
align-items: end;
margin-bottom: 1rem;
}
.voiced-history__export {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.voiced-history__row {
cursor: pointer;
}
.voiced-history__totals {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 2rem;
padding: 0.75rem 1rem;
background: var(--cds-layer-accent);
border-top: 1px solid var(--cds-border-subtle);
}
.voiced-detail__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
.voiced-detail__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
}
.voiced-detail__grid {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
gap: 2rem;
align-items: start;
}
@media (max-width: 1100px) {
.voiced-detail__grid,
.voiced-history__filters {
grid-template-columns: 1fr;
}
}
.voiced-detail__h2 {
font-size: 1.25rem;
font-weight: 400;
margin-bottom: 0.75rem;
}
.voiced-detail__parties {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.voiced-detail__field {
margin-bottom: 0.75rem;
}
.voiced-detail__pre {
white-space: pre-line;
}
.voiced-detail__totals {
margin: 1rem 0 0.5rem auto;
max-width: 22rem;
}
.voiced-detail__totals > div {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
}
.voiced-detail__grand {
border-top: 1px solid var(--cds-border-strong);
font-weight: 600;
}
.voiced-detail__preview {
position: sticky;
top: 4rem;
}
.voiced-detail__unarchived {
padding: 1.5rem;
background: var(--cds-layer-accent);
}
.voiced-detail__unarchived p {
margin-bottom: 1rem;
}