- 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.
701 lines
12 KiB
SCSS
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;
|
|
}
|