Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39130f41e6 | ||
|
|
e02d219736 | ||
|
|
bfb9845c46 |
@@ -0,0 +1,163 @@
|
||||
<!-- Pull pages off a copier. Chrome follows the MMD design system locked on 2026-08-28:
|
||||
a front-window title bar (white to silver, 3px red top edge, ink text), 4px corners,
|
||||
flat buttons, one red fill for the single action the person came to do. Every colour and
|
||||
radius is a theme key from Themes/MMD.xaml - nothing is picked here. -->
|
||||
<Window x:Class="MmdPdf.Features.Scan.ScanDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Scan a document"
|
||||
Width="520" SizeToContent="Height"
|
||||
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
|
||||
Background="Transparent"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"
|
||||
UseLayoutRounding="True" SnapsToDevicePixels="True"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Border Background="{DynamicResource BgFlyout}"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title bar: the front-window treatment from the design system -->
|
||||
<Border Grid.Row="0" Height="34"
|
||||
Background="{DynamicResource WindowFrontTitleBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="0,3,0,0"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid Margin="12,0,8,0">
|
||||
<TextBlock x:Name="TitleText" Text="Scan a document" VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI" FontSize="12.5" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextBrush}"/>
|
||||
<Button x:Name="CloseChrome" Click="Cancel_Click" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" Width="24" Height="24"
|
||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
Foreground="{DynamicResource TextBrush}" Content="✕"
|
||||
FontFamily="Segoe UI" FontSize="11"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<Grid Grid.Row="1" Margin="16,14,16,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="COPIER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="CopierBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="FEED" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="FeedBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="COLOUR" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="ColourBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="DETAIL" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="DetailBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="PAPER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="PaperBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="AFTERWARDS" VerticalAlignment="Top" Margin="0,2,0,0"
|
||||
FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<StackPanel Grid.Column="1">
|
||||
<CheckBox x:Name="SearchableBox" IsChecked="True" FontSize="11.5">
|
||||
<TextBlock Text="Make the words searchable, in French and English"
|
||||
TextWrapping="Wrap" MaxWidth="180"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="StraightenBox" Margin="0,6,0,0" FontSize="11.5">
|
||||
<TextBlock Text="Straighten crooked pages" TextWrapping="Wrap" MaxWidth="180"/>
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Sheet preview: what one page will look like -->
|
||||
<Border Grid.Column="1" Width="132" Margin="16,0,0,0"
|
||||
Background="{DynamicResource BgCanvas}"
|
||||
BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Border Width="96" Height="132" Margin="10" VerticalAlignment="Center"
|
||||
Background="White" BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1">
|
||||
<StackPanel Margin="9,9,8,0">
|
||||
<Rectangle Height="3" Width="56" HorizontalAlignment="Left" Margin="0,0,0,5"
|
||||
Fill="{DynamicResource DimTextBrush}"/>
|
||||
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="66" HorizontalAlignment="Left" Margin="0,0,0,4"
|
||||
Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="44" HorizontalAlignment="Left" Margin="0,0,0,4"
|
||||
Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="60" HorizontalAlignment="Left" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource SurfaceBrush}"
|
||||
BorderBrush="{DynamicResource PaneBorderBrush}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="0,0,4,4" Padding="16,10">
|
||||
<Grid>
|
||||
<TextBlock x:Name="FootNote" VerticalAlignment="Center" FontSize="10.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"
|
||||
Text="Pages arrive as one document you can edit"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="CancelBtn" Content="Cancel" Click="Cancel_Click"
|
||||
Height="28" Padding="13,0" Margin="0,0,8,0" FontSize="11.5" FontWeight="SemiBold"/>
|
||||
<Button x:Name="ScanBtn" Content="Scan" Click="Scan_Click"
|
||||
Height="28" Padding="13,0" FontSize="11.5" FontWeight="SemiBold"
|
||||
Background="{DynamicResource PrimaryBrush}"
|
||||
Foreground="{DynamicResource OnPrimaryBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="1"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using MmdPdf.Services;
|
||||
|
||||
namespace MmdPdf.Features.Scan
|
||||
{
|
||||
/// <summary>
|
||||
/// Asks the copier for pages. The person picks the machine, the feed, how much ink and how
|
||||
/// much detail; everything else is the copier's own default. The pages come back as image
|
||||
/// files which MainWindow turns into one document.
|
||||
///
|
||||
/// A run cannot be interrupted mid-sheet - that is the copier's hardware, not a decision made
|
||||
/// here - so Cancel stops after the sheet in progress and keeps the pages already read.
|
||||
/// </summary>
|
||||
public partial class ScanDialog : Window
|
||||
{
|
||||
private readonly List<ScannerInfo> _scanners;
|
||||
private CancellationTokenSource? _cancel;
|
||||
private bool _running;
|
||||
|
||||
/// <summary>Image files, one per page, in the order the copier read them.</summary>
|
||||
internal string[] Pages { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>The person asked for the words to be made searchable afterwards.</summary>
|
||||
internal bool MakeSearchable { get; private set; }
|
||||
|
||||
/// <summary>The person asked for crooked pages to be straightened afterwards.</summary>
|
||||
internal bool Straighten { get; private set; }
|
||||
|
||||
public ScanDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_scanners = ScanService.IsAvailable ? ScanService.ListScanners() : new List<ScannerInfo>();
|
||||
|
||||
foreach (var s in _scanners) CopierBox.Items.Add(s.Name);
|
||||
if (_scanners.Count > 0)
|
||||
{
|
||||
CopierBox.SelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
CopierBox.Items.Add(ScanService.IsAvailable
|
||||
? "No copier found on this machine"
|
||||
: "This machine has no imaging service");
|
||||
CopierBox.SelectedIndex = 0;
|
||||
CopierBox.IsEnabled = false;
|
||||
ScanBtn.IsEnabled = false;
|
||||
FootNote.Text = "Switch the copier on, or connect it, then open this again";
|
||||
}
|
||||
|
||||
FeedBox.Items.Add("Feeder, one side");
|
||||
FeedBox.Items.Add("Feeder, both sides");
|
||||
FeedBox.Items.Add("Glass, one sheet");
|
||||
FeedBox.SelectedIndex = 0;
|
||||
|
||||
ColourBox.Items.Add("Black and white");
|
||||
ColourBox.Items.Add("Shades of grey");
|
||||
ColourBox.Items.Add("Colour");
|
||||
ColourBox.SelectedIndex = 0;
|
||||
|
||||
DetailBox.Items.Add("200 dpi, quick");
|
||||
DetailBox.Items.Add("300 dpi, good for text");
|
||||
DetailBox.Items.Add("600 dpi, fine detail");
|
||||
DetailBox.SelectedIndex = 1;
|
||||
|
||||
foreach (var p in ScanService.PaperSizes) PaperBox.Items.Add(p);
|
||||
PaperBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState == MouseButtonState.Pressed) DragMove();
|
||||
}
|
||||
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
_cancel?.Cancel();
|
||||
FootNote.Text = "Stopping after this sheet…";
|
||||
return;
|
||||
}
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void Scan_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_running || _scanners.Count == 0) return;
|
||||
|
||||
var device = _scanners[Math.Max(0, CopierBox.SelectedIndex)];
|
||||
var feed = FeedBox.SelectedIndex switch
|
||||
{
|
||||
1 => ScanFeed.FeederBothSides,
|
||||
2 => ScanFeed.Flatbed,
|
||||
_ => ScanFeed.FeederOneSide,
|
||||
};
|
||||
var colour = ColourBox.SelectedIndex switch
|
||||
{
|
||||
1 => ScanColour.Greyscale,
|
||||
2 => ScanColour.Colour,
|
||||
_ => ScanColour.BlackAndWhite,
|
||||
};
|
||||
int dpi = DetailBox.SelectedIndex switch
|
||||
{
|
||||
0 => 200,
|
||||
2 => 600,
|
||||
_ => 300,
|
||||
};
|
||||
string paper = PaperBox.SelectedItem as string ?? "A4 portrait";
|
||||
|
||||
MakeSearchable = SearchableBox.IsChecked == true;
|
||||
Straighten = StraightenBox.IsChecked == true;
|
||||
|
||||
_running = true;
|
||||
_cancel = new CancellationTokenSource();
|
||||
ScanBtn.IsEnabled = false;
|
||||
CancelBtn.Content = "Stop";
|
||||
SetInputsEnabled(false);
|
||||
FootNote.Text = feed == ScanFeed.Flatbed ? "Reading the sheet on the glass…" : "Reading…";
|
||||
|
||||
var progress = new Progress<int>(n =>
|
||||
FootNote.Text = n == 1 ? "1 page read" : n.ToString(CultureInfo.InvariantCulture) + " pages read");
|
||||
|
||||
try
|
||||
{
|
||||
var token = _cancel.Token;
|
||||
var reporter = (IProgress<int>)progress;
|
||||
string[] pages = await Task.Run(() =>
|
||||
ScanService.Acquire(device.DeviceId, feed, colour, dpi, paper,
|
||||
maxPages: feed == ScanFeed.Flatbed ? 1 : 200,
|
||||
cancel: token,
|
||||
onPage: reporter.Report).ToArray(), token);
|
||||
|
||||
Pages = pages;
|
||||
if (pages.Length == 0)
|
||||
{
|
||||
FootNote.Text = "The copier returned no pages";
|
||||
ResetAfterRun();
|
||||
return;
|
||||
}
|
||||
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
FootNote.Text = "Stopped";
|
||||
ResetAfterRun();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FootNote.Text = "The copier could not be used: " + ex.Message;
|
||||
ResetAfterRun();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAfterRun()
|
||||
{
|
||||
_running = false;
|
||||
_cancel?.Dispose();
|
||||
_cancel = null;
|
||||
ScanBtn.IsEnabled = _scanners.Count > 0;
|
||||
CancelBtn.Content = "Cancel";
|
||||
SetInputsEnabled(true);
|
||||
}
|
||||
|
||||
private void SetInputsEnabled(bool on)
|
||||
{
|
||||
CopierBox.IsEnabled = on && _scanners.Count > 0;
|
||||
FeedBox.IsEnabled = on;
|
||||
ColourBox.IsEnabled = on;
|
||||
DetailBox.IsEnabled = on;
|
||||
PaperBox.IsEnabled = on;
|
||||
SearchableBox.IsEnabled = on;
|
||||
StraightenBox.IsEnabled = on;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
# HANDOVER — MMD PDF (Windows PDF editor for staff)
|
||||
|
||||
**Written** 2026-08-27 15:18:53 +04 (11:18 UTC)
|
||||
**Session** mmd-projects-87 · **Repo** `gitea.baobab-ts.com/mmd-it/mmd-pdf`
|
||||
|
||||
---
|
||||
|
||||
## What this is
|
||||
|
||||
MMD staff had no sanctioned way to write inside PDF documents. PDFgear was
|
||||
blocked on 2026-07-20 for forging the Windows default-PDF registry setting.
|
||||
This session picked a replacement, proved it, forked it, and started turning it
|
||||
into an MMD product.
|
||||
|
||||
**MMD PDF** = GPL-3.0 fork of KillerPDF 1.7.5, staying GPL-3.0, `FORK-NOTICE.md`
|
||||
in the repo records what changed.
|
||||
|
||||
---
|
||||
|
||||
## What was done
|
||||
|
||||
### Chosen and proven (this part is solid)
|
||||
|
||||
- Surveyed open-source Windows PDF editors. **No MIT-licensed app does true
|
||||
inline PDF text editing** — the only genuinely MIT desktop option (every-pdf)
|
||||
can only stamp text on top of a page. KillerPDF (GPL-3.0) was picked; GPL
|
||||
restricts forking and redistribution, not internal deployment to staff.
|
||||
- **Passed the check that killed PDFgear.** `.pdf` UserChoice ProgId and Hash
|
||||
identical before and after install and after opening a PDF; the protective
|
||||
deny ACE intact; one signed executable, no helper binary. Read live from the
|
||||
registry on our own machine.
|
||||
- **Found and fixed a real defect.** Editing existing text left the original
|
||||
glyphs in the file — a delivery note corrected from 12 to 17 tonnes returned
|
||||
*both* figures from `pdftotext`. `Services/PdfRedactText.cs` now walks the
|
||||
page content stream, tracks the CTM and text matrix, and blanks every
|
||||
show-text operator whose origin sits inside the cover rectangle, before any
|
||||
new content is appended. Proven: after the fix the saved file contains only
|
||||
the new figure, every other line intact and still searchable.
|
||||
- **Built the test rig that found it.** `/opt/mmd-wintest/wintest` on the
|
||||
hypervisor: Xvfb + xfreerdp3 + xdotool, so every action is a real pointer or
|
||||
key event with no scripting back door. This is how user testing is done from
|
||||
now on.
|
||||
|
||||
### Dedicated Windows test machine
|
||||
|
||||
VM 203 on **mmd-fsn-03** renamed **mmd-win-test-01**, `onboot=1`, described in
|
||||
Proxmox. Windows Server 2022, 20 cores / 72 GB, guest 10.10.20.22. Its DNS was
|
||||
broken (no internet at all) — fixed. .NET SDK 8 installed for builds.
|
||||
It still also carries the Mali plant-station build, so it is shared.
|
||||
|
||||
### Design — failed review, not accepted
|
||||
|
||||
The rebrand and the new MMD shell design system ("red plate") were applied and
|
||||
merged **before any design existed**, which is the wrong order and Ben called
|
||||
it. Redone properly afterwards: design system pulled from the Claude Design
|
||||
project, red-plate stylesheet lifted verbatim from the session that owns it,
|
||||
seven screens composed at 800×600, then put through an **independent adversarial
|
||||
review** — the gate that was missing.
|
||||
|
||||
**Verdict: DESIGN-FAIL.** 3 blockers, 5 major, 3 minor. All eleven findings and
|
||||
the fix state are in `docs/mmd-pdf/SIGNOFFS.md`.
|
||||
|
||||
---
|
||||
|
||||
## Current live / verified state
|
||||
|
||||
| Thing | State |
|
||||
|---|---|
|
||||
| `main` | carries the rebrand + the redaction fix (PR #1, merged) |
|
||||
| `design/official-logo-and-tokens-20260827` | **bfb9845, pushed, NOT merged** — official logo + design-system values. Unaccepted: the design failed review. |
|
||||
| Build | `dotnet build -c Release`, 0 errors, on mmd-win-test-01 |
|
||||
| App | runs; opens, edits and saves correctly, keyboard-and-mouse tested |
|
||||
| Design | **DESIGN-FAIL**, no re-review run, no Ben sign-off |
|
||||
| Scanning | not written |
|
||||
| Signing / Intune | not started |
|
||||
|
||||
Claude Design project `446a3931-c345-48da-a3ac-246af02a2e52` ("Design System"):
|
||||
the repo's live CDS 0.3.40 was pushed into `mmd-portal/` (its mirror was stale
|
||||
at 0.3.11), and round-1 design pages into `mmd-pdf/`. **Round 2 was not pushed**
|
||||
— it failed review.
|
||||
|
||||
Two artifact links exist from before Ben's `/login` and are **dead**
|
||||
(`65c7726d…`, `e47d705f…`). Live under the current account:
|
||||
`7d8d6b56-ef36-4729-8e88-a3fb1392707e` (round-1 canvas) and
|
||||
`a7a974e8-aff4-49e6-b4cc-f80414769515` (status page). Both are round-1 and both
|
||||
predate the review — do not present either as reviewed.
|
||||
|
||||
---
|
||||
|
||||
## Resume steps
|
||||
|
||||
1. **Read** `docs/mmd-pdf/SIGNOFFS.md` for the eleven findings, then
|
||||
`BACKLOG.md` for what is ⛔.
|
||||
2. **Get Ben's toolbar pick** — A, B or C on the Directions board. The
|
||||
independent reviewer recommends **C** (three tabs; nothing hidden, every tool
|
||||
named). Nothing else in the design can be finished without it.
|
||||
3. **Apply the seven unapplied findings** — 1, 6, 7, 8, 9, 10, 11 — the big one
|
||||
being the rebuild of Main / EditText / PageRail / Nothing onto Direction C.
|
||||
Working files: `docs/mmd-pdf/design/build.py`, regenerate with `python3
|
||||
build.py`, render each `.dc.html` locally and look at it.
|
||||
4. **Re-run the independent review** with the prompt in
|
||||
`docs/mmd-pdf/TEST-CASES.md` under "To review a design". Give the reviewer
|
||||
the **complete** palette this time, including `#FAFAF8` and `#C9C6BC` — my
|
||||
incomplete brief caused half of finding 4.
|
||||
5. **Only after DESIGN-PASS**, publish the canvas and hand Ben the link for
|
||||
annotation. His annotation closes the gate; the reviewer's PASS does not.
|
||||
6. **Then** re-derive `Themes/MMD.xaml` from the signed-off design and merge
|
||||
`design/official-logo-and-tokens-20260827`.
|
||||
7. **Then** scanning (NAPS2.Wia, MIT, .NET Framework 4.6.2 — compatible with
|
||||
this net48 codebase), then signing, then Intune.
|
||||
|
||||
---
|
||||
|
||||
## Key facts, commits, addresses
|
||||
|
||||
- Repo `gitea.baobab-ts.com/mmd-it/mmd-pdf`; `main` = PR #1 merged;
|
||||
branch `design/official-logo-and-tokens-20260827` = **bfb9845**.
|
||||
- Hypervisor **mmd-fsn-03** — Hetzner dedicated, Falkenstein FSN1-DC16, auction
|
||||
#3049905, public 168.119.88.172, tailnet 100.64.0.19, Proxmox VE 9.2.10.
|
||||
Reach it with `ssh -i ~/.ssh/id_ed25519_mmd_fsn03_rescue root@100.64.0.19`.
|
||||
- Test machine **mmd-win-test-01** (VM 203), guest 10.10.20.22. Humans reach it
|
||||
in a browser at `https://plantpc.baobab-ts.com` (Guacamole, Entra SSO).
|
||||
- Source on the VM at `C:\src`; build output
|
||||
`C:\src\bin\Release\net48\MmdPdf.exe`; .NET SDK at `C:\build\dotnet`.
|
||||
- Graph for Intune/Entra: **MMD-CIO-Agent client secret** at
|
||||
`~/mmd-projects/entra-cio-agent/secret.env`, helper `graphlib.mjs`. The
|
||||
certificate in `~/.config/mmd/certs` is Exchange PowerShell only — do not
|
||||
reach for it and then conclude the agent lacks Intune access.
|
||||
|
||||
---
|
||||
|
||||
## Open items
|
||||
|
||||
- **Ben: pick the toolbar direction.** Blocks everything else in the design.
|
||||
- **Ben: the 16–24px app icon.** No official small MMD mark exists anywhere. The
|
||||
icon crops the lockup's baobab disc, which reads from 32px and is muddy below
|
||||
it. An agent must not draw a simplified one. Commission or accept muddy.
|
||||
- Seven design findings unapplied; no re-review run.
|
||||
- Scanning not written. Signing not started. Intune not started.
|
||||
- mmd-win-test-01's Windows evaluation licence — a restart reset it to 180 days,
|
||||
but it needs a real licence before it is a standing fixture.
|
||||
- Design round 2 not pushed to the Claude Design project (it failed review).
|
||||
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
1. **`Stop-Process` over WinRM against a running GUI app hangs the session.**
|
||||
Launch GUI apps with a *new* scheduled-task name instead.
|
||||
2. **`freerdp2-x11` does not exist on Debian 13.** It is `freerdp3-x11`, and the
|
||||
binary is `xfreerdp3`, not `xfreerdp`.
|
||||
3. **The word-mark was two literal `<Run Text="Killer">` elements**, not the
|
||||
product-name token — a global rename misses it. Check the title bar visually
|
||||
after any rename.
|
||||
4. **`redplate.css` belongs to another session.** It is lifted verbatim from
|
||||
their `build.mjs`. I edited it once chasing a review finding and reverted it.
|
||||
Do not edit it; raise a discrepancy with the owning session instead.
|
||||
5. **A `/login` mid-session orphans artifacts** published earlier under the old
|
||||
account. Republishing the same *file path* keeps mapping to the dead one —
|
||||
copy the file to a new path to claim a fresh URL.
|
||||
6. **`Ctrl+A` inside an inline text edit selects all annotations**, not the
|
||||
text. Use `End` + `BackSpace`. `Enter` commits, `Escape` cancels — the status
|
||||
bar now says so because both were guessed wrong.
|
||||
7. **Flatten is not redaction's friend.** It removes the old text by turning the
|
||||
page into a picture, destroying every searchable word. That is why the
|
||||
content-stream fix exists.
|
||||
8. **The design gate is not closed by a reviewer's PASS.** Only Ben's
|
||||
annotation closes it, and design that never went through the proper lane is
|
||||
an automatic fail however good it looks.
|
||||
@@ -112,12 +112,12 @@
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Width" Value="36"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Width" Value="30"/>
|
||||
<Setter Property="Height" Value="26"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
@@ -231,6 +231,49 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Solid tool button on the band: flat, 1px border, 4px corner. The one place a
|
||||
white fill appears on the red ground, so the tool it carries reads as the
|
||||
action always within reach. -->
|
||||
<Style x:Key="BandButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#FFFFFF"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="#FFFFFF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,0"/>
|
||||
<Setter Property="Height" Value="24"/>
|
||||
<Setter Property="Margin" Value="0,0,10,0"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="10.5"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F1F1EC"/>
|
||||
<Setter Property="BorderBrush" Value="#F1F1EC"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#E4E3DC"/>
|
||||
<Setter Property="BorderBrush" Value="#E4E3DC"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.55"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ChromeButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource CaptionGlyphBrush}"/>
|
||||
@@ -1179,10 +1222,10 @@
|
||||
<!-- Soft drop shadow: a blurred, offset copy behind the crisp wordmark -->
|
||||
<StackPanel Orientation="Horizontal" Margin="1,1,0,0" Opacity="{DynamicResource WordmarkShadowOpacity}">
|
||||
<StackPanel.Effect><BlurEffect Radius="2"/></StackPanel.Effect>
|
||||
<TextBlock><Run Text="MMD " FontFamily="{DynamicResource WordmarkFont}" FontSize="16.5" Foreground="#D8000000"/><Run Text="PDF" FontFamily="{DynamicResource WordmarkFontPdf}" FontWeight="Bold" FontSize="21.45" Foreground="#D8000000"/></TextBlock>
|
||||
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock><Run Text="MMD " FontFamily="{DynamicResource WordmarkFont}" FontSize="16.5" Foreground="{DynamicResource TextBrush}"/><Run Text="PDF" FontFamily="{DynamicResource WordmarkFontPdf}" FontWeight="Bold" FontSize="21.45" Foreground="{DynamicResource AccentLogo}"/></TextBlock>
|
||||
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -1192,6 +1235,15 @@
|
||||
Margin="16,0,12,0" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{DynamicResource FilenameBrush}" FontFamily="Consolas" FontSize="11"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="{DynamicResource CaptionButtonsMargin}">
|
||||
<Button x:Name="BandScanBtn" Click="Scan_Click" Style="{StaticResource BandButton}"
|
||||
ToolTip="Pull pages off a copier"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Scan" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Click="MinimizeBtn_Click" Style="{StaticResource ChromeButton}"
|
||||
Foreground="{DynamicResource CaptionGlyphBrush}"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
@@ -1237,7 +1289,7 @@
|
||||
<Border IsHitTestVisible="False" Opacity="{DynamicResource GrainOpacity}" Background="{StaticResource GrainBrushShared}"/>
|
||||
<!-- Background=Transparent is load-bearing: without it the grid's empty stretches have no
|
||||
hit surface and the right-click appearance picker only opens over the buttons. -->
|
||||
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,2" Background="Transparent">
|
||||
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,1" Background="Transparent">
|
||||
<Grid.ContextMenu>
|
||||
<!-- Items are built in code (BuildToolbarMenu, SettingsPanel.cs) so captions re-Loc on
|
||||
language switch; attached here so WPF supplies the inheritance context that lets
|
||||
|
||||
@@ -532,6 +532,22 @@ namespace MmdPdf
|
||||
// squared chrome on every move (and once now that the handle exists).
|
||||
LocationChanged += OnWindowLocationChanged;
|
||||
UpdateWindowChrome();
|
||||
FitToSmallScreen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plant machines run at 800 by 600. The default 1000 by 700 window does not fit one, and
|
||||
/// a window taller than the screen puts its own title bar - and therefore Scan, the window
|
||||
/// buttons and the tabs - above the top edge where nobody can reach them. On a screen that
|
||||
/// cannot hold the default, start maximised instead.
|
||||
/// </summary>
|
||||
private void FitToSmallScreen()
|
||||
{
|
||||
if (WindowState != WindowState.Normal) return;
|
||||
var work = System.Windows.SystemParameters.WorkArea;
|
||||
if (work.Width <= 0 || work.Height <= 0) return;
|
||||
if (Width <= work.Width && Height <= work.Height) return;
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\mmd-icon.ico" />
|
||||
<Resource Include="Resources\mmd-icon.png" />
|
||||
<Resource Include="Resources\mmd-lockup-red.png" />
|
||||
<Resource Include="Resources\mmd-lockup-white.png" />
|
||||
<Resource Include="Fonts\Typewriter-A602.ttf" />
|
||||
<EmbeddedResource Include="Resources\pdf-file.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,373 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace MmdPdf.Services
|
||||
{
|
||||
/// <summary>Which side of the paper the copier reads, and from where.</summary>
|
||||
internal enum ScanFeed
|
||||
{
|
||||
Flatbed,
|
||||
FeederOneSide,
|
||||
FeederBothSides,
|
||||
}
|
||||
|
||||
/// <summary>How much ink the copier records.</summary>
|
||||
internal enum ScanColour
|
||||
{
|
||||
BlackAndWhite,
|
||||
Greyscale,
|
||||
Colour,
|
||||
}
|
||||
|
||||
/// <summary>One copier the machine can reach.</summary>
|
||||
internal sealed class ScannerInfo
|
||||
{
|
||||
internal ScannerInfo(string deviceId, string name)
|
||||
{
|
||||
DeviceId = deviceId;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
internal string DeviceId { get; }
|
||||
internal string Name { get; }
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls pages off a network copier or a desk scanner through WIA, the imaging service that
|
||||
/// ships with Windows. Nothing is installed for this: the COM objects are created late-bound
|
||||
/// by ProgID, so a machine with no imaging stack fails at <see cref="IsAvailable"/> instead of
|
||||
/// at load time, and the application still starts.
|
||||
///
|
||||
/// The page images land in a folder of their own under the user's temp directory and are
|
||||
/// handed back as file paths; turning them into a document is PdfImport's job, the same way
|
||||
/// an imported photograph is handled.
|
||||
/// </summary>
|
||||
internal static class ScanService
|
||||
{
|
||||
// WIA item property ids. These are the documented constants; the COM interface takes
|
||||
// them as integers, so they are written here once rather than repeated as magic numbers.
|
||||
private const int WIA_IPS_CUR_INTENT = 6146;
|
||||
private const int WIA_IPS_XRES = 6147;
|
||||
private const int WIA_IPS_YRES = 6148;
|
||||
private const int WIA_IPS_XPOS = 6149;
|
||||
private const int WIA_IPS_YPOS = 6150;
|
||||
private const int WIA_IPS_XEXTENT = 6151;
|
||||
private const int WIA_IPS_YEXTENT = 6152;
|
||||
private const int WIA_IPA_DATATYPE = 4103;
|
||||
private const int WIA_IPA_DEPTH = 4104;
|
||||
|
||||
// Device properties that drive the sheet feeder.
|
||||
private const int WIA_DPS_DOCUMENT_HANDLING_SELECT = 3088;
|
||||
private const int WIA_DPS_DOCUMENT_HANDLING_STATUS = 3087;
|
||||
private const int WIA_DPS_PAGES = 3096;
|
||||
|
||||
private const int FEEDER = 0x001;
|
||||
private const int FLATBED = 0x002;
|
||||
private const int DUPLEX = 0x004;
|
||||
private const int FEED_READY = 0x001;
|
||||
|
||||
// Intent values: what the copier should optimise for.
|
||||
private const int INTENT_COLOUR = 0x00000001;
|
||||
private const int INTENT_GREYSCALE = 0x00000002;
|
||||
private const int INTENT_TEXT = 0x00000004;
|
||||
|
||||
private const string FORMAT_PNG = "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}";
|
||||
|
||||
// Paper the copier is asked for, in inches. A4 is the MMD standard; Letter is here
|
||||
// because the Dubai office buys it locally.
|
||||
private static readonly Dictionary<string, (double W, double H)> PaperInches =
|
||||
new Dictionary<string, (double, double)>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["A4 portrait"] = (8.27, 11.69),
|
||||
["A4 landscape"] = (11.69, 8.27),
|
||||
["A5 portrait"] = (5.83, 8.27),
|
||||
["Letter portrait"] = (8.5, 11.0),
|
||||
["Legal portrait"] = (8.5, 14.0),
|
||||
};
|
||||
|
||||
internal static IEnumerable<string> PaperSizes => PaperInches.Keys;
|
||||
|
||||
/// <summary>True when this machine has the Windows imaging service at all.</summary>
|
||||
internal static bool IsAvailable => Type.GetTypeFromProgID("WIA.DeviceManager") != null;
|
||||
|
||||
/// <summary>
|
||||
/// Every copier and scanner the machine can currently reach. An empty list is a normal
|
||||
/// answer - it means nothing is switched on - and never an exception the caller has to
|
||||
/// catch.
|
||||
/// </summary>
|
||||
internal static List<ScannerInfo> ListScanners()
|
||||
{
|
||||
var found = new List<ScannerInfo>();
|
||||
var progId = Type.GetTypeFromProgID("WIA.DeviceManager");
|
||||
if (progId == null) return found;
|
||||
|
||||
object? manager = null;
|
||||
try
|
||||
{
|
||||
manager = Activator.CreateInstance(progId);
|
||||
dynamic dm = manager!;
|
||||
dynamic infos = dm.DeviceInfos;
|
||||
int count = (int)infos.Count;
|
||||
for (int i = 1; i <= count; i++) // WIA collections are 1-based
|
||||
{
|
||||
dynamic info = infos[i];
|
||||
try
|
||||
{
|
||||
// 1 == ScannerDeviceType. Cameras and video devices are skipped.
|
||||
if ((int)info.Type != 1) continue;
|
||||
string name = ReadProperty(info.Properties, "Name") ?? "Copier";
|
||||
found.Add(new ScannerInfo((string)info.DeviceID, name));
|
||||
}
|
||||
finally { Release(info); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { /* imaging service not running: nothing to offer */ }
|
||||
catch (InvalidCastException) { }
|
||||
finally { Release(manager); }
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads pages and writes each one as a PNG into its own folder. Returns the files in
|
||||
/// page order. Cancelling stops after the page in progress - a copier cannot be
|
||||
/// interrupted mid-sheet.
|
||||
/// </summary>
|
||||
/// <param name="onPage">Called with the running page count as each sheet lands.</param>
|
||||
internal static List<string> Acquire(
|
||||
string deviceId,
|
||||
ScanFeed feed,
|
||||
ScanColour colour,
|
||||
int dpi,
|
||||
string paper,
|
||||
int maxPages,
|
||||
CancellationToken cancel,
|
||||
Action<int>? onPage = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) throw new ArgumentException("No copier chosen.", nameof(deviceId));
|
||||
if (dpi < 75 || dpi > 1200) throw new ArgumentOutOfRangeException(nameof(dpi));
|
||||
|
||||
var progId = Type.GetTypeFromProgID("WIA.DeviceManager")
|
||||
?? throw new InvalidOperationException("This machine has no imaging service.");
|
||||
|
||||
string folder = Path.Combine(Path.GetTempPath(), "MmdPdf.Scan." + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
var pages = new List<string>();
|
||||
object? manager = null;
|
||||
object? device = null;
|
||||
try
|
||||
{
|
||||
manager = Activator.CreateInstance(progId);
|
||||
dynamic dm = manager!;
|
||||
dynamic infos = dm.DeviceInfos;
|
||||
|
||||
dynamic? chosen = null;
|
||||
int count = (int)infos.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic info = infos[i];
|
||||
if (string.Equals((string)info.DeviceID, deviceId, StringComparison.Ordinal)) { chosen = info; break; }
|
||||
Release(info);
|
||||
}
|
||||
if (chosen == null) throw new InvalidOperationException("That copier is no longer reachable.");
|
||||
|
||||
device = chosen.Connect();
|
||||
dynamic dev = device!;
|
||||
ConfigureFeeder(dev, feed, maxPages);
|
||||
|
||||
dynamic item = dev.Items[1];
|
||||
try
|
||||
{
|
||||
ConfigurePage(item, colour, dpi, paper);
|
||||
|
||||
for (int page = 1; page <= maxPages; page++)
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
string path = Path.Combine(folder, "page-" + page.ToString("000", CultureInfo.InvariantCulture) + ".png");
|
||||
try
|
||||
{
|
||||
dynamic image = item.Transfer(FORMAT_PNG);
|
||||
try
|
||||
{
|
||||
image.SaveFile(path);
|
||||
pages.Add(path);
|
||||
onPage?.Invoke(pages.Count);
|
||||
}
|
||||
finally { Release(image); }
|
||||
}
|
||||
catch (COMException ex) when (IsOutOfPaper(ex))
|
||||
{
|
||||
break; // the feeder emptied: a normal end, not a failure
|
||||
}
|
||||
|
||||
if (feed == ScanFeed.Flatbed) break; // one sheet on the glass
|
||||
if (!FeederHasPaper(dev)) break;
|
||||
}
|
||||
}
|
||||
finally { Release(item); }
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(device);
|
||||
Release(manager);
|
||||
}
|
||||
|
||||
if (pages.Count == 0)
|
||||
{
|
||||
try { Directory.Delete(folder, true); } catch (IOException) { }
|
||||
catch (UnauthorizedAccessException) { }
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
private static void ConfigureFeeder(dynamic device, ScanFeed feed, int maxPages)
|
||||
{
|
||||
int handling = feed == ScanFeed.Flatbed ? FLATBED : FEEDER;
|
||||
if (feed == ScanFeed.FeederBothSides) handling |= DUPLEX;
|
||||
|
||||
TrySetProperty(device.Properties, WIA_DPS_DOCUMENT_HANDLING_SELECT, handling);
|
||||
if (feed != ScanFeed.Flatbed) TrySetProperty(device.Properties, WIA_DPS_PAGES, maxPages);
|
||||
}
|
||||
|
||||
private static void ConfigurePage(dynamic item, ScanColour colour, int dpi, string paper)
|
||||
{
|
||||
int intent = colour switch
|
||||
{
|
||||
ScanColour.Colour => INTENT_COLOUR,
|
||||
ScanColour.Greyscale => INTENT_GREYSCALE,
|
||||
_ => INTENT_TEXT,
|
||||
};
|
||||
int depth = colour switch
|
||||
{
|
||||
ScanColour.Colour => 24,
|
||||
ScanColour.Greyscale => 8,
|
||||
_ => 1,
|
||||
};
|
||||
int dataType = colour switch
|
||||
{
|
||||
ScanColour.Colour => 3, // WIA_DATA_COLOR
|
||||
ScanColour.Greyscale => 2, // WIA_DATA_GRAYSCALE
|
||||
_ => 0, // WIA_DATA_THRESHOLD
|
||||
};
|
||||
|
||||
TrySetProperty(item.Properties, WIA_IPS_CUR_INTENT, intent);
|
||||
TrySetProperty(item.Properties, WIA_IPA_DATATYPE, dataType);
|
||||
TrySetProperty(item.Properties, WIA_IPA_DEPTH, depth);
|
||||
TrySetProperty(item.Properties, WIA_IPS_XRES, dpi);
|
||||
TrySetProperty(item.Properties, WIA_IPS_YRES, dpi);
|
||||
|
||||
var size = PaperInches.TryGetValue(paper ?? string.Empty, out var found) ? found : PaperInches["A4 portrait"];
|
||||
TrySetProperty(item.Properties, WIA_IPS_XPOS, 0);
|
||||
TrySetProperty(item.Properties, WIA_IPS_YPOS, 0);
|
||||
TrySetProperty(item.Properties, WIA_IPS_XEXTENT, (int)Math.Round(size.W * dpi));
|
||||
TrySetProperty(item.Properties, WIA_IPS_YEXTENT, (int)Math.Round(size.H * dpi));
|
||||
}
|
||||
|
||||
private static bool FeederHasPaper(dynamic device)
|
||||
{
|
||||
try
|
||||
{
|
||||
object? raw = ReadPropertyValue(device.Properties, WIA_DPS_DOCUMENT_HANDLING_STATUS);
|
||||
if (raw == null) return false;
|
||||
return ((int)Convert.ToInt32(raw, CultureInfo.InvariantCulture) & FEED_READY) == FEED_READY;
|
||||
}
|
||||
catch (COMException) { return false; }
|
||||
catch (InvalidCastException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>The copier saying "the tray is empty", which ends a run rather than failing it.</summary>
|
||||
private static bool IsOutOfPaper(COMException ex)
|
||||
{
|
||||
const int WIA_ERROR_PAPER_EMPTY = unchecked((int)0x80210003);
|
||||
const int WIA_ERROR_PAPER_JAM = unchecked((int)0x80210002);
|
||||
return ex.ErrorCode == WIA_ERROR_PAPER_EMPTY || ex.ErrorCode == WIA_ERROR_PAPER_JAM;
|
||||
}
|
||||
|
||||
private static string? ReadProperty(dynamic properties, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if (string.Equals((string)p.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
return Convert.ToString(p.get_Value(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? ReadPropertyValue(dynamic properties, int propertyId)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if ((int)p.PropertyID == propertyId) return p.get_Value();
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets one property if the copier has it. Copiers differ wildly in what they expose, so a
|
||||
/// missing or read-only property is ignored rather than failing the run - the copier then
|
||||
/// uses its own default for that setting.
|
||||
/// </summary>
|
||||
private static void TrySetProperty(dynamic properties, int propertyId, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if ((int)p.PropertyID != propertyId) continue;
|
||||
if ((bool)p.IsReadOnly) return;
|
||||
p.set_Value(value);
|
||||
return;
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
}
|
||||
|
||||
private static void Release(object? comObject)
|
||||
{
|
||||
if (comObject == null) return;
|
||||
try
|
||||
{
|
||||
if (Marshal.IsComObject(comObject)) Marshal.ReleaseComObject(comObject);
|
||||
}
|
||||
catch (ArgumentException) { }
|
||||
catch (InvalidComObjectException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using MmdPdf.Controls;
|
||||
using MmdPdf.Features.Scan;
|
||||
|
||||
namespace MmdPdf
|
||||
{
|
||||
// Pulling pages off a copier. The dialog does the talking to the hardware; this file only
|
||||
// turns the pages it hands back into a document, which is the same route an imported
|
||||
// photograph takes (ImportAndZip.cs).
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void Scan_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new ScanDialog { Owner = this };
|
||||
bool? answer;
|
||||
try
|
||||
{
|
||||
answer = dlg.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
KillerDialog.Show(this, "The copier could not be reached.\n" + ex.Message,
|
||||
"MMD PDF", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer != true || dlg.Pages.Length == 0) return;
|
||||
|
||||
string name = "Scan " + DateTime.Now.ToString("d MMM yyyy HH:mm", CultureInfo.CurrentCulture);
|
||||
OpenImagesAsImportedTab(dlg.Pages, name);
|
||||
|
||||
// "Make the words searchable" is the copier setting a person ticks before the run;
|
||||
// the reading itself happens here, on the document that just opened, through the
|
||||
// text recognition the application already carries.
|
||||
if (dlg.MakeSearchable) MakeSearchablePdf();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +590,7 @@ namespace MmdPdf
|
||||
// Text only: caption, no icon (nothing to shed - there'd be nothing left).
|
||||
if (textOnly)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
|
||||
btn.Content = new TextBlock
|
||||
{
|
||||
Text = Loc(key),
|
||||
@@ -604,7 +604,7 @@ namespace MmdPdf
|
||||
// Text beside the icon, while it still fits.
|
||||
if (beside && withLabel)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
@@ -628,7 +628,7 @@ namespace MmdPdf
|
||||
// Text under the icon: the icon stacked over a small caption, while it still fits.
|
||||
if (under && withLabel)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 56 : 52; btn.Padding = new Thickness(6, 4, 6, 4);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 45 : 42; btn.Padding = new Thickness(6, 2, 6, 2);
|
||||
var col = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
col.Children.Add(new TextBlock
|
||||
{
|
||||
|
||||
@@ -3,17 +3,55 @@
|
||||
xmlns:po="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<!-- MMD Steel Group corporate theme. The only theme this application ships.
|
||||
Palette is the Employee Portal's: C8102E red used sparingly, 23221E ink,
|
||||
F1F1EC platinum ground, FFFFFF paper. Square edges, no grain, no shadows. -->
|
||||
<!-- MMD Steel Group corporate theme - the only theme this application ships.
|
||||
Values are the new MMD shell design system ("red plate", light-only, no dark
|
||||
variant): white work area, silver chrome, desk grey behind the document,
|
||||
23221E ink, C8102E red partitioned to primary action / focus / urgency only.
|
||||
Square by default. Held for Ben's sign-off: dark table header rows and the
|
||||
window-chrome variant (V2 vs V7) are still being chosen. -->
|
||||
|
||||
<!-- The one merged top bar, locked 2026-08-28. MMD red, the white lockup on it,
|
||||
and the window buttons at its right end. These are the design system's own
|
||||
values; no colour here is new. -->
|
||||
<LinearGradientBrush x:Key="TitleBarBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#d4213f"/>
|
||||
<GradientStop Offset="0.55" Color="#c8102e"/>
|
||||
<GradientStop Offset="1.0" Color="#a50d26"/>
|
||||
</LinearGradientBrush>
|
||||
<sys:Double x:Key="TitleBarHeight">42</sys:Double>
|
||||
<Thickness x:Key="TitleBarPadding">14,0,0,0</Thickness>
|
||||
<SolidColorBrush x:Key="CaptionGlyphBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<sys:Double x:Key="CaptionButtonWidth">24</sys:Double>
|
||||
<sys:Double x:Key="CaptionButtonHeight">24</sys:Double>
|
||||
<Thickness x:Key="CaptionButtonsMargin">0,0,9,0</Thickness>
|
||||
<!-- The window buttons sit on the red band, so they are white. The close button
|
||||
inverts on hover - white plate, red cross - because red on red is invisible,
|
||||
which is exactly what the first build of this band showed. -->
|
||||
<SolidColorBrush x:Key="CaptionButtonBrush" Color="#1fffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionHoverBrush" Color="#33ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseHoverBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseHoverFgBrush" Color="#c8102e" po:Freeze="True"/>
|
||||
<sys:Double x:Key="WordmarkShadowOpacity">0</sys:Double>
|
||||
<sys:Double x:Key="TitleIconSize">0</sys:Double>
|
||||
<Thickness x:Key="TitleIconMargin">0</Thickness>
|
||||
|
||||
<!-- A window that is in front, for the dialogs: white to silver with a red top edge.
|
||||
The bevel lives on title bars only; buttons are flat. -->
|
||||
<LinearGradientBrush x:Key="WindowFrontTitleBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#ffffff"/>
|
||||
<GradientStop Offset="0.62" Color="#ffffff"/>
|
||||
<GradientStop Offset="1.0" Color="#f1f1ec"/>
|
||||
</LinearGradientBrush>
|
||||
<sys:Double x:Key="FooterHeight">26</sys:Double>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PaneBrush" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="RowHoverBrush" Color="#ecece6" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MenuHoverBrush" Color="#ecece6" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BgRecentPanel" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PaneBrush" Color="#e4e3dc" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="RowHoverBrush" Color="#f7f7f3" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MenuHoverBrush" Color="#f7f7f3" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="#f3f3f0" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BgRecentPanel" Color="#f3f3f0" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BgFlyout" Color="#ffffff" po:Freeze="True"/>
|
||||
<LinearGradientBrush x:Key="TabStripFadeBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#0023221e"/>
|
||||
@@ -21,12 +59,12 @@
|
||||
<GradientStop Offset="1.0" Color="#2223221e"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="MenuBackgroundBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#e6e5df" po:Freeze="True"/>
|
||||
|
||||
<!-- Accent. Red carries the active tool and the one primary action, nothing else. -->
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#c8102e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#ad0e28" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PrimaryPressedBrush" Color="#920c21" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#a80d24" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PrimaryPressedBrush" Color="#8e0b20" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="OnPrimaryBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="OutlineBtnBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="HeaderLineBrush" Color="#23221e" po:Freeze="True"/>
|
||||
@@ -35,39 +73,39 @@
|
||||
<SolidColorBrush x:Key="InstallBtnBg" Color="#f1f1ec" po:Freeze="True"/>
|
||||
|
||||
<!-- Borders -->
|
||||
<SolidColorBrush x:Key="MenuBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DropBorder" Color="#b9b9b0" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PaneBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="AppBorderBrush" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MenuBorderBrush" Color="#d7d6ce" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DropBorder" Color="#b9b8ae" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PaneBorderBrush" Color="#d7d6ce" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="AppBorderBrush" Color="#b9b8ae" po:Freeze="True"/>
|
||||
|
||||
<!-- Selection: dark on light, the way the portal tables read. -->
|
||||
<SolidColorBrush x:Key="RowSelectedBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="OutlinePressedBrush" Color="#f6e0e4" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="OutlinePressedBrush" Color="#fbeef0" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SelectionBg" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SelectionFg" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SettingsOpenRowBg" Color="#ecece6" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SettingsOpenRowBg" Color="#f1f1ec" po:Freeze="True"/>
|
||||
|
||||
<!-- Type -->
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MutedTextBrush" Color="#5c5a54" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MutedTextBrush" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MenuTextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ChromeTextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="FilenameBrush" Color="#5c5a54" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DimTextBrush" Color="#6f6d66" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TextFooter" Color="#8b8880" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ChromeTextBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="FilenameBrush" Color="#e8ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DimTextBrush" Color="#767368" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TextFooter" Color="#6b6a64" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DangerRed" Color="#c8102e" po:Freeze="True"/>
|
||||
|
||||
<!-- Furniture -->
|
||||
<SolidColorBrush x:Key="BgDragHandle" Color="#b9b9b0" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DragLine" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BgDragHandle" Color="#c7c6bd" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DragLine" Color="#d7d6ce" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#5523221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ScrollThumbHoverBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TableHeaderBrush" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="RowAltBrush" Color="#0a23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="InputBorderBrush" Color="#c9c9c1" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="RowAltBrush" Color="#0823221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="InputBorderBrush" Color="#c7c6bd" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="InputHoverBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TreeLineBrush" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SliderTrack" Color="#d9d9d2" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TreeLineBrush" Color="#e6e5df" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="SliderTrack" Color="#d7d6ce" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="RadioAccent" Color="#c8102e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BgOverlay" Color="#8823221e" po:Freeze="True"/>
|
||||
|
||||
@@ -77,29 +115,32 @@
|
||||
<sys:Double x:Key="HeaderShadowOpacity">0</sys:Double>
|
||||
|
||||
<!-- The workspace behind the page. Platinum, one step darker than the page itself. -->
|
||||
<SolidColorBrush x:Key="BgCanvas" Color="#e4e4de" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#f6e0e4" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="BgCanvas" Color="#d4d0c6" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#fbeef0" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#ecece6" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#23221e" po:Freeze="True"/>
|
||||
|
||||
<!-- Shortcut keyboard view. Graded ink rather than a rainbow; red marks editing only. -->
|
||||
<SolidColorBrush x:Key="KsCatFile" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatTools" Color="#5c5a54" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatTools" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatEdit" Color="#c8102e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatNav" Color="#6f6d66" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatView" Color="#8b8880" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatSearch" Color="#5c5a54" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatHelp" Color="#8b8880" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatOcr" Color="#6f6d66" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatNav" Color="#767368" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatView" Color="#6b6a64" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatSearch" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatHelp" Color="#6b6a64" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="KsCatOcr" Color="#767368" po:Freeze="True"/>
|
||||
|
||||
<!-- No text stroke on a light corporate surface. -->
|
||||
<DropShadowEffect x:Key="TextStroke" Color="Black" BlurRadius="0" ShadowDepth="0" Direction="270" Opacity="0"/>
|
||||
|
||||
<!-- Square. The portal's chrome is square-edged; rounded corners are not MMD. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="FlyoutCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="AccentSwatchCornerRadius">0</CornerRadius>
|
||||
<!-- Four-pixel corners, per the design system locked 2026-08-28. The window itself
|
||||
stays square at the top because the band runs edge to edge when maximised, which
|
||||
is how these machines are used. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="FlyoutCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="AccentSwatchCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="TitleBarCornerRadius">0</CornerRadius>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# MMD PDF — feature backlog
|
||||
|
||||
Status: 🟢 proven end-to-end on a real build with evidence · 🟡 in progress ·
|
||||
⚪ not started · ⛔ blocked. A green build or a passing unit test is not 🟢.
|
||||
|
||||
| ID | Feature | Ver | Owner | Status | Sign-off | PRD / Screen / Test |
|
||||
|---|---|---|---|---|---|---|
|
||||
| PDF-01 | Open and read a PDF | 1.7.5 | kua-agent | 🟢 | unsigned | PRD · SCREENS Main · TC-01 |
|
||||
| PDF-02 | Change text already on the page | 1.7.5 | kua-agent | 🟢 | unsigned | PRD · SCREENS EditText · TC-02 |
|
||||
| PDF-03 | **The old text is removed, not covered** | 0.1 | kua-agent | 🟢 | unsigned | PRD req.1 · SCREENS Save · TC-03 |
|
||||
| PDF-04 | Save over the original, with confirmation | 1.7.5 | kua-agent | 🟢 | unsigned | SCREENS Save · TC-04 |
|
||||
| PDF-05 | Runs with no admin rights, portable | 1.7.5 | kua-agent | 🟢 | unsigned | PRD req.4 · TC-05 |
|
||||
| PDF-06 | Leaves the Windows default-PDF setting alone | 1.7.5 | kua-agent | 🟢 | unsigned | PRD "why" · TC-06 |
|
||||
| PDF-07 | MMD name, icon and word-mark | 0.1 | kua-agent | 🟡 | unsigned | SCREENS all · TC-07 |
|
||||
| PDF-08 | MMD shell design system, light only, no theme picker | 0.1 | kua-agent | ⛔ | unsigned | PRD req.3 · SCREENS · TC-08 |
|
||||
| PDF-09 | Works at 800×600 | 0.1 | — | ⛔ | unsigned | PRD req.2 · SCREENS Directions · TC-09 |
|
||||
| PDF-10 | Fill and sign forms | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-10 |
|
||||
| PDF-11 | Mark up: highlight, draw, shapes, stamps, images | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-11 |
|
||||
| PDF-12 | Page work: merge, extract, delete, reorder, crop | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-12 |
|
||||
| PDF-13 | Turn a scan into searchable text | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-13 |
|
||||
| PDF-14 | Pull pages off a network copier | — | — | ⚪ | unsigned | PRD in-scope · SCREENS Scan · TC-14 |
|
||||
| PDF-15 | French interface | 1.7.5 | — | ⚪ | unsigned | PRD req.5 · TC-15 |
|
||||
| PDF-16 | Print | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-16 |
|
||||
| PDF-17 | Code signing under an MMD certificate | — | — | ⚪ | unsigned | PRD open q. · TC-17 |
|
||||
| PDF-18 | Deploy through Intune | — | — | ⚪ | unsigned | PRD open q. · TC-18 |
|
||||
| PDF-19 | Official square app mark for 16–24px | — | — | ⛔ | unsigned | PRD open q. |
|
||||
|
||||
## Why the ⛔
|
||||
|
||||
- **PDF-08, PDF-09** — the design failed an independent review on 2026-08-27
|
||||
(3 blockers, 5 major). Nothing may be built on it until it passes and Ben
|
||||
signs it. Findings and the fix state are in `SIGNOFFS.md`.
|
||||
- **PDF-19** — no official small mark exists anywhere in MMD's artwork, and only
|
||||
Ben can authorise commissioning one. An agent must not draw it.
|
||||
|
||||
## Evidence behind the 🟢 rows
|
||||
|
||||
All on **mmd-win-test-01** (VM 203, hypervisor mmd-fsn-03, Falkenstein),
|
||||
2026-08-27, driven by keyboard and mouse only through
|
||||
`/opt/mmd-wintest/wintest`. Screenshots in `design/shots/` are design mockups,
|
||||
not this evidence; the run itself is recorded in Hindsight under tag
|
||||
`mmd-pdf` + `live-verified`.
|
||||
@@ -0,0 +1,74 @@
|
||||
# MMD PDF — product requirements
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Version | 0.1 |
|
||||
| Date | 2026-08-27 |
|
||||
| Sign-off | **unsigned** |
|
||||
| Repo | `gitea.baobab-ts.com/mmd-it/mmd-pdf` |
|
||||
|
||||
## What it is
|
||||
|
||||
A PDF editor for MMD staff on Windows. Free, open source, no subscription, no
|
||||
account, nothing sent anywhere. It is a GPL-3.0 fork of KillerPDF 1.7.5 and
|
||||
stays GPL-3.0; `FORK-NOTICE.md` records what MMD changed.
|
||||
|
||||
## Why it exists
|
||||
|
||||
Staff need to write inside PDF documents — fill a delivery note, correct a
|
||||
figure, sign a form, mark up a drawing, turn a scan into searchable text.
|
||||
Until now there was no sanctioned tool. PDFgear was evaluated on 2026-07-20 and
|
||||
blocked: it forged the Windows default-PDF registry setting behind the consent
|
||||
screen, confirmed by its own paid auditor.
|
||||
|
||||
## Who uses it
|
||||
|
||||
- **Plant and office staff** across Dakar, Bamako, Kigali, Luanda, Conakry,
|
||||
Ouagadougou and Dubai. Not computer people. Many read French, not English.
|
||||
- **Their screens are 800×600.** Every surface must work at that size. This is
|
||||
a hard constraint, not a target.
|
||||
|
||||
## In scope
|
||||
|
||||
- Read a PDF, and change text that is already on the page.
|
||||
- Fill and sign forms, including comb fields, checkboxes and radio groups.
|
||||
- Mark up: highlight, draw, shapes, stamps, images, signatures.
|
||||
- Page work: merge, extract, delete, reorder, crop, rotate.
|
||||
- Turn a scan into searchable text (on-device, eleven languages).
|
||||
- Pull pages straight off a network copier. **Not built.**
|
||||
- Print.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Anything that leaves the machine. No cloud, no account, no telemetry.
|
||||
- Collaborative editing.
|
||||
- Creating documents from scratch — that is Word's job.
|
||||
|
||||
## Hard requirements
|
||||
|
||||
1. **An edited figure must be gone from the file, not covered over.** Upstream
|
||||
painted a white box over the original text and drew the replacement on top;
|
||||
extracting text returned both values. A corrected quantity on a delivery
|
||||
note that leaves the building must not carry the old number inside it.
|
||||
2. **800×600.** See above.
|
||||
3. **The MMD shell design system** ("red plate"), light only, official MMD
|
||||
lockup, red reserved for one primary action, focus, and urgency.
|
||||
4. **No admin rights needed** to install or run.
|
||||
5. **French and English** interface at minimum; the upstream base ships twelve
|
||||
locales.
|
||||
|
||||
## Data
|
||||
|
||||
The user's own files on their own machine. Nothing else. No network calls
|
||||
except the optional download of a higher-accuracy text-recognition language
|
||||
pack, and the scanner on the local network once that is built.
|
||||
|
||||
## Open questions
|
||||
|
||||
- No official square MMD app mark exists. The icon crops the lockup's baobab
|
||||
disc; it reads from 32px and is muddy at 16–24px.
|
||||
- Which of three toolbar treatments (see `SCREENS.md`) — the independent
|
||||
reviewer recommends C.
|
||||
- Code signing. Re-signing under an MMD certificate resets the reputation the
|
||||
upstream signed build has, and SmartScreen is set to block without an
|
||||
override on the pilot group.
|
||||
@@ -0,0 +1,118 @@
|
||||
# MMD PDF — screens
|
||||
|
||||
Every screen is 800×600. Budget: 28px title bar + 20–23px status bar leaves
|
||||
about 550px for the toolbar and the page.
|
||||
|
||||
Design source files: `design/*.dc.html`, generated by `design/build.py` on the
|
||||
red-plate stylesheet `design/redplate.css` (lifted verbatim from the MMD shell
|
||||
design system, not retyped). Renders: `design/shots/*.png`.
|
||||
|
||||
**These are mockups. The design failed independent review on 2026-08-27 and is
|
||||
not approved. Do not build from them yet.**
|
||||
|
||||
---
|
||||
|
||||
## Directions — the toolbar decision
|
||||
|
||||
Not a screen; a comparison sheet. A PDF editor carries about thirty tools and
|
||||
thirty named tools need roughly 1400px of row. Three ways out:
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| A - one row, named tools |
|
||||
| [Open][Save][Print][Scan][OCR] | [Select][Text][Mark]... [Fit][…]|
|
||||
| 40px chrome · 512px page · 16 of 30 tools behind the "…" |
|
||||
+------------------------------------------------------------------+
|
||||
| B - icons only, nothing hidden |
|
||||
| [][][][][][][][][][][][][][][][][][][][][] [Fit - +] |
|
||||
| 30px chrome · 522px page · nothing hidden, everything unlabelled |
|
||||
+------------------------------------------------------------------+
|
||||
| C - three tabs, named tools, nothing hidden |
|
||||
| DOCUMENT | EDIT | PAGES |
|
||||
| [Select][Text][Mark][Draw][Shape][Sign][Stamp][Image] [Fit - +] |
|
||||
| 56px chrome · 496px page · nothing hidden, one click deeper |
|
||||
+------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
Independent reviewer's recommendation: **C**. A is the one drawn on every other
|
||||
screen and it hides sixteen tools behind an unlabelled "…" button; B asks a
|
||||
first-time clerk to guess thirty glyphs.
|
||||
|
||||
---
|
||||
|
||||
## Main — a document open
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| [MMD] MMD PDF / BON-2026-0847 delivery note.pdf - [] X | 28px
|
||||
+------------------------------------------------------------------+
|
||||
| [Open][Save][Print][Scan][OCR] | [Select][Text][Mark][Draw] | 40px
|
||||
| [Shape][Sign] | [Pages][Search][Undo] [FIT WIDTH - +] […] |
|
||||
+---+--------------------------------------------------------------+
|
||||
| > | |
|
||||
| 1 | +--------------------------------------+ |
|
||||
| | | MMD STEEL GROUP - DELIVERY NOTE | |
|
||||
| | | | |
|
||||
| | | Supplier: Fabrimetal SARL | |
|
||||
| | | Reference: BON-2026-0847 | |
|
||||
| | | Quantity delivered: 12 tonnes | |
|
||||
| | +--------------------------------------+ |
|
||||
+---+--------------------------------------------------------------+
|
||||
| Ready 1 OF 1 · FIT WIDTH · 100% | 23px
|
||||
+------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
- **Title bar** — official MMD lockup at 13px, then the application name in
|
||||
Fraunces, then the file name. Window controls right.
|
||||
- **Tool strip** — grouped: document actions, then editing tools, then
|
||||
navigation. The armed tool is a solid red fill with white glyph; that is the
|
||||
only red on the screen.
|
||||
- **Closed page strip** (24–26px) — a chevron to open the page list and the
|
||||
page you are on. It reclaims 62px over the open rail, which matters at 800px.
|
||||
- **Page** — white on the F3F3F0 field, hairline border, no shadow.
|
||||
- **Status bar** — what the application is doing, left; where you are, right.
|
||||
|
||||
## EditText — changing a figure on the page
|
||||
|
||||
Same as Main plus a **context bar** (28px) that exists only while the Text tool
|
||||
is armed: colour swatches, size, bold/italic, and the sentence
|
||||
"Enter keeps it · Esc throws it away" — both were guessed wrong in testing.
|
||||
|
||||
The edited run is boxed in red on a red wash. The status bar turns red and
|
||||
says **"The old quantity is removed from the file when you save."** That is the
|
||||
moment the original is destroyed and it must be unmissable.
|
||||
|
||||
## PageRail — the page list open
|
||||
|
||||
The 88px rail replaces the closed strip: `PAGES / MARKS` headers, then one
|
||||
thumbnail per page. The page you are on carries a 3px red left edge.
|
||||
|
||||
## Nothing — no document open
|
||||
|
||||
The editing tools are dimmed to the label tone. The work area becomes a dense
|
||||
table of recent documents: reference, document, pages, state. Row click opens.
|
||||
Two actions in the section header: `Open a file…` outlined, `Scan a document`
|
||||
the one red fill.
|
||||
|
||||
## Save — writing over the original
|
||||
|
||||
The application stays visible and dimmed behind. The dialog is white, ink-ruled
|
||||
title, and says in plain words what changes:
|
||||
|
||||
```
|
||||
Save
|
||||
Write the change into BON-2026-0847 delivery note.pdf?
|
||||
WAS Quantity delivered: 12 tonnes
|
||||
BECOMES Quantity delivered: 19 tonnes
|
||||
THE OLD TEXT is taken out of the file, not covered over
|
||||
[ Cancel ] [ Keep both ] [ Replace ]
|
||||
```
|
||||
|
||||
`Replace` is the one red fill — the destructive action is correctly primary
|
||||
because it is what the person came to do. **Open finding:** "Keep both" does not
|
||||
yet say what filename it produces.
|
||||
|
||||
## Scan — pulling pages off a copier
|
||||
|
||||
**Not built.** Same dialog shape: copier, feed, colour, detail, and text-search
|
||||
on by default in French and English. `Scan` is the one red fill.
|
||||
@@ -0,0 +1,71 @@
|
||||
# MMD PDF — versions and sign-off log
|
||||
|
||||
Only Ben signs. An agent's own screenshots, a green build, or an independent
|
||||
reviewer's PASS are all evidence — none of them is a signature.
|
||||
|
||||
## Log
|
||||
|
||||
| Date | Feature | Ver | Event | Who | Evidence |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-27 | PDF-01…06 | 1.7.5 / 0.1 | proven end-to-end, keyboard and mouse only, on mmd-win-test-01 | kua-agent | Hindsight `mmd-pdf` + `live-verified` |
|
||||
| 2026-08-27 | PDF-03 | 0.1 | `12 tonnes` no longer recoverable from the saved file; `19 tonnes` present; other lines intact | kua-agent | `pdftotext` before/after on the VM |
|
||||
| 2026-08-27 | PDF-06 | 1.7.5 | `.pdf` UserChoice ProgId + Hash unchanged; deny ACE intact; no helper binary | kua-agent | registry read before and after install and first open |
|
||||
| 2026-08-27 | PDF-07…09 | 0.1 | **DESIGN-FAIL** — independent adversarial review, 3 blockers, 5 major, 3 minor | independent reviewer (separate agent, default-FAIL) | findings below |
|
||||
| 2026-08-27 | — | — | **awaiting Ben** | — | — |
|
||||
|
||||
## The design gate — OPEN
|
||||
|
||||
### DESIGN-FAIL, 2026-08-27, round 1
|
||||
|
||||
**Blockers**
|
||||
|
||||
1. The `···` overflow control is an unlabelled icon-only button and the only
|
||||
way to sixteen of thirty tools. Icon-only is permitted for window controls
|
||||
only. → label it, or ship Direction C which hides nothing.
|
||||
2. Every toolbar tool name is 7.5px — below the reading floor, on the one screen
|
||||
size that matters, for the exact text the design's argument rests on.
|
||||
→ 9–10px minimum; if fourteen names then stop fitting, that is the argument
|
||||
for C over A.
|
||||
3. Disabled tools use `#A9A69C` — off-palette and lighter than the `#6B6A64`
|
||||
floor. → `#767368`.
|
||||
|
||||
**Major**
|
||||
|
||||
4. Eleven off-palette values across the set. *(Partly my bad brief: `#FAFAF8`
|
||||
and `#C9C6BC` are legitimate design-system tokens I failed to give the
|
||||
reviewer. `#C9C7C0` on the page thumbnails is genuinely mine and wrong.)*
|
||||
5. The whole mono-label scale runs 7.5–8.5px, including the data-loss warning
|
||||
meant to stop a clerk destroying a figure. → 9–9.5px floor.
|
||||
6. `OCR` is an acronym sitting among plain words. → "Read text".
|
||||
7. Direction A hides sixteen tools; B asks a first-timer to guess thirty
|
||||
glyphs. **Reviewer picks C.** → rebuild the four screens on C.
|
||||
8. The recent-documents table leaves a dead band above the status bar.
|
||||
|
||||
**Minor**
|
||||
|
||||
9. Page thumbnails show generic grey bars, not the real page.
|
||||
10. The toolbar's first icon and the status bar's first text do not share a
|
||||
left column (~14–17px vs ~9–10px).
|
||||
11. "Keep both" does not say what filename it creates.
|
||||
|
||||
**What the reviewer said works:** the Save and Scan dialogs each keep exactly
|
||||
one red fill for the one primary action, with plain button words and the
|
||||
destructive action correctly primary; Save states the consequence in plain
|
||||
language; the recent table matches the table spec exactly; square corners, no
|
||||
gradients or grain, Fraunces for titles and mono for data throughout.
|
||||
|
||||
### Fix state at handover
|
||||
|
||||
Applied: findings 2, 3, 5 (type scale to a 9.5px floor; disabled tone to
|
||||
`#767368`), and the `#C9C7C0` half of 4. Also fixed before the review landed:
|
||||
two competing red fills per dialog, dialogs floating on an empty field, the
|
||||
selection edge painting down every cell, and the stacked "1 / 1".
|
||||
|
||||
Reverted deliberately: I had edited `redplate.css`, the design system's own
|
||||
stylesheet, to chase finding 4. That file is another session's and is lifted
|
||||
verbatim — it has been restored from source. `#FAFAF8` and `#C9C6BC` stay.
|
||||
|
||||
Not applied: **1, 6, 7, 8, 9, 10, 11** — including the rebuild onto Direction C,
|
||||
which is the big one and needs Ben's pick first.
|
||||
|
||||
**No re-review has run.** The design is DESIGN-FAIL as it stands.
|
||||
@@ -0,0 +1,148 @@
|
||||
# MMD PDF — test cases, test data, agent tasking
|
||||
|
||||
## The rig
|
||||
|
||||
Testing happens on **mmd-win-test-01** — VM 203 on hypervisor mmd-fsn-03
|
||||
(Falkenstein, 168.119.88.172 / tailnet 100.64.0.19), Windows Server 2022,
|
||||
guest 10.10.20.22. It auto-starts with the host.
|
||||
|
||||
**A user test uses keyboard and mouse only.** The rig is
|
||||
`/opt/mmd-wintest/wintest` on the hypervisor — Xvfb + xfreerdp3 + xdotool, so
|
||||
every input is a real pointer or key event. There is no scripting back door in
|
||||
that path.
|
||||
|
||||
```
|
||||
wintest start | stop | status | shot <name>
|
||||
wintest move <x> <y> · click [1|2|3] · dblclick · at <x> <y> [btn]
|
||||
wintest drag <x1> <y1> <x2> <y2> · scroll up|down [n]
|
||||
wintest type "<text>" · key <keys...>
|
||||
```
|
||||
|
||||
`/root/winrun.py` (PowerShell over WinRM) exists for **setup and for reading a
|
||||
result back** — installing, copying a file in, extracting text from a saved
|
||||
PDF. It is never used to make something happen inside the application.
|
||||
|
||||
Gotcha: `Stop-Process` on a running GUI app hangs the WinRM session. Launch GUI
|
||||
apps with a **new** scheduled-task name:
|
||||
`schtasks /create /tn X /tr "..." /ru Administrator /it /f` then `/run`.
|
||||
|
||||
## Test data
|
||||
|
||||
`/root/kpdf/mkpdf.py` on the hypervisor writes a one-page delivery note:
|
||||
|
||||
```
|
||||
MMD STEEL GROUP - DELIVERY NOTE
|
||||
Supplier: Fabrimetal SARL
|
||||
Reference: BON-2026-0847
|
||||
Quantity delivered: 12 tonnes
|
||||
Received by: ____________________
|
||||
Editing test: change the quantity above to 14 tonnes.
|
||||
```
|
||||
|
||||
Serve it to the VM from the hypervisor (the VM reaches 10.10.20.1, and now the
|
||||
internet too after its DNS was fixed on 2026-08-27):
|
||||
`cd /root/kpdf && python3 -m http.server 8899 --bind 10.10.20.1`
|
||||
|
||||
---
|
||||
|
||||
## TC-01 · Open and read a PDF — PDF-01
|
||||
|
||||
**Input** Launch `MmdPdf.exe` with `C:\src\clean-note.pdf` as its argument.
|
||||
**Expected** Window opens; the page renders; the status bar reads `1 OF 1`; the
|
||||
title bar carries the file name.
|
||||
|
||||
## TC-02 · Change text already on the page — PDF-02
|
||||
|
||||
**Input** Double-click the words "Quantity delivered: 12 tonnes". Press `End`,
|
||||
nine × `BackSpace`, type `19 tonnes`, press `Enter`.
|
||||
**Expected** The line reads "Quantity delivered: 19 tonnes" on screen.
|
||||
|
||||
## TC-03 · The old text is removed, not covered — PDF-03 · **the one that matters**
|
||||
|
||||
**Input** After TC-02, click Save, answer `Replace`. Then read the file back:
|
||||
`pdftotext delivery-note.pdf -`
|
||||
**Expected** The extracted text contains `19 tonnes` and **does not contain**
|
||||
`12 tonnes`. Every other line is unchanged and still extractable.
|
||||
**Falsification** Before the fix this returned both figures. If `12 tonnes`
|
||||
appears, the fix has regressed and the tool must not be used to correct figures.
|
||||
|
||||
## TC-04 · Save asks before it overwrites — PDF-04
|
||||
|
||||
**Input** Edit anything, click Save.
|
||||
**Expected** A dialog naming the file. Nothing is written to disk until it is
|
||||
answered. `Cancel` leaves the file byte-identical.
|
||||
|
||||
## TC-05 · No admin rights, portable — PDF-05
|
||||
|
||||
**Input** Copy the single `.exe` to a folder in a standard user's profile and
|
||||
run it.
|
||||
**Expected** It runs. No elevation prompt. Nothing is written to Program Files.
|
||||
|
||||
## TC-06 · Leaves the Windows default-PDF setting alone — PDF-06 · **security**
|
||||
|
||||
**Input** Read `HKCU:\...\FileExts\.pdf\UserChoice` (ProgId, Hash) and the
|
||||
key's ACL. Install, launch, open a PDF. Read them again.
|
||||
**Expected** ProgId, Hash and the deny ACE are **identical**. No helper binary
|
||||
appears beside the executable.
|
||||
**Falsification** PDFgear forged this hash and stripped the deny ACE. If either
|
||||
changes, the tool is blocked, as PDFgear was.
|
||||
**Read-only** — this test only reads the registry.
|
||||
|
||||
## TC-07 · MMD name, icon and word-mark — PDF-07
|
||||
|
||||
**Input** Look at the title bar, the taskbar icon, the About box, and Explorer's
|
||||
icon for the file type.
|
||||
**Expected** MMD PDF everywhere; the official lockup in the title bar; no trace
|
||||
of the upstream product name or author. Icon legible at 32px and above.
|
||||
|
||||
## TC-08 · MMD design system, light only — PDF-08 · ⛔ blocked
|
||||
|
||||
**Input** Open Settings; look for a theme or accent picker. Sample the chrome,
|
||||
rail, page-field and ink colours.
|
||||
**Expected** No picker exists. Values match `Themes/MMD.xaml`. No dark surface
|
||||
anywhere. Windows title bar stays light.
|
||||
**Blocked** until the design passes review and Ben signs it.
|
||||
|
||||
## TC-09 · Works at 800×600 — PDF-09 · ⛔ blocked
|
||||
|
||||
**Input** Set the VM to 800×600. Open a document. Reach every tool.
|
||||
**Expected** No horizontal scrollbar on the chrome. Every tool reachable. Every
|
||||
label a person must read is at least 9px. The page is visible without scrolling
|
||||
its own width.
|
||||
**Blocked** on the toolbar direction (A/B/C).
|
||||
|
||||
## TC-10 … TC-18 — not written
|
||||
|
||||
Forms, mark-up, page work, text recognition, scanning, French, print, signing
|
||||
and Intune are ⚪ in the backlog. Write the case when the feature is picked up;
|
||||
do not pre-write cases for work nobody is doing.
|
||||
|
||||
---
|
||||
|
||||
## Agent tasking
|
||||
|
||||
**To run a user test** (TC-01 … TC-09):
|
||||
|
||||
> You are testing MMD PDF on mmd-win-test-01. Reach the hypervisor with
|
||||
> `ssh -i ~/.ssh/id_ed25519_mmd_fsn03_rescue root@100.64.0.19`. Start the rig
|
||||
> with `wintest start`. **Every action inside the application must be a real
|
||||
> keyboard or mouse event through `wintest`** — never PowerShell, never a
|
||||
> registry write, never a command-line argument to make something happen.
|
||||
> `/root/winrun.py` is allowed only to put a file on the VM, launch the app, and
|
||||
> read a result back. Run <TC-nn> exactly as written. Screenshot before and
|
||||
> after every step with `wintest shot`, decode each PNG and **look at it**.
|
||||
> Report each step as it happened, including anything that did not work. If you
|
||||
> cannot perform a step, say `blocked` and name the exact interaction — do not
|
||||
> substitute a different route to the same end.
|
||||
|
||||
**To review a design** (any UI change):
|
||||
|
||||
> You are an independent design reviewer. You did not build this. Your default
|
||||
> verdict is DESIGN-FAIL; only evidence moves you off it. Read the screenshots
|
||||
> as images. Judge against: 800×600, light only, the fixed palette, red only for
|
||||
> one primary action / focus / urgency, square corners, the type scale, a 9px
|
||||
> floor on any text a person must read, no icon-only buttons except the window
|
||||
> controls, dense but not cramped, and whether a plant clerk who is not a
|
||||
> computer person could use it. Output a verdict line then numbered findings,
|
||||
> each with severity, the screen, the specific element, and the concrete fix.
|
||||
> DESIGN-PASS only with zero blockers and zero majors.
|
||||
@@ -0,0 +1,224 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
||||
<style>
|
||||
|
||||
body{margin:0;background:#F3F3F0;font-family:"PP Neue Machina","Neue Machina",system-ui,-apple-system,"Segoe UI",sans-serif;color:#3C3A34;-webkit-font-smoothing:antialiased}
|
||||
a{color:#C8102E} a:hover{color:#8E0B20}
|
||||
*{box-sizing:border-box}
|
||||
.root{padding:16px;background:#F3F3F0}
|
||||
.shell{width:1180px;display:flex;flex-direction:column;font-size:11.5px;background:#FFFFFF;border:1px solid #C7C6BD}
|
||||
.s-title{display:flex;align-items:center;gap:10px;padding:0 10px;height:30px;background:#C8102E;color:#fff;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px}
|
||||
.s-title .mmd-logo{height:19px;width:auto;display:block;flex:none;fill:#fff}
|
||||
.spring{flex:1}
|
||||
.s-who{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.07em;text-transform:uppercase;color:rgba(255,255,255,0.85)}
|
||||
.s-signout{font-size:10px;padding:1px 0;color:#fff;text-decoration:underline;text-underline-offset:2px}
|
||||
.s-bar{display:flex;align-items:stretch;gap:2px;padding:0 5px;background:#F7F7F3;border-bottom:1px solid #D7D6CE}
|
||||
.s-item{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;white-space:nowrap;border:1px solid transparent;color:#3C3A34}
|
||||
.s-item .cv{font-size:8px;opacity:0.5}
|
||||
.s-item .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;padding:0 4px;border:1px solid #23221E;color:#23221E;background:#FFFFFF;line-height:13px}
|
||||
.s-item.on{background:#FFFFFF;border-color:#D7D6CE;border-top:2px solid #C8102E;border-bottom-color:#FFFFFF;margin-bottom:-1px;font-weight:700;color:#23221E}
|
||||
.s-item.on .n{border-color:#23221E;color:#23221E}
|
||||
.s-tools{margin-left:auto;display:flex;align-items:center;gap:6px;padding-right:4px}
|
||||
.s-search{width:168px;padding:2px 7px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
|
||||
.s-btn{padding:2px 9px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
|
||||
.s-desk{position:relative;background:#D4D0C6;overflow:hidden}
|
||||
.s-desk.tiled{display:grid;grid-template-columns:1.35fr 1fr;grid-template-rows:auto auto;gap:10px;padding:10px;align-items:start}
|
||||
.s-desk.stacked{height:492px}
|
||||
.s-desk.oneup{padding:10px}
|
||||
.s-task{display:flex;align-items:center;gap:10px;padding:4px 8px;font-size:10.5px;background:#F1F1EC;border-top:1px solid #D7D6CE}
|
||||
.s-task .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.s-group{display:inline-flex;align-items:stretch;border:1px solid #D7D6CE;background:#FFFFFF}
|
||||
.s-group .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:2px 7px;border-right:1px solid #D7D6CE;display:flex;align-items:center;background:#F7F7F3}
|
||||
.s-group .win{padding:2px 8px;font-size:10px;color:#23221E;border-right:1px solid #D7D6CE;display:flex;align-items:center;gap:5px;background:#F1F1EC}
|
||||
.s-group .win:last-child{border-right:0}
|
||||
.s-group .win.on{box-shadow:inset 3px 0 0 #C8102E;font-weight:700;background:#FFFFFF}
|
||||
.s-group .win.min{color:#3C3A34;background:#FFFFFF;outline:1px dashed #6B6A64;outline-offset:-3px}
|
||||
|
||||
.s-task .tile{font-size:10px;padding:1px 8px;border:1px solid #C7C6BD;background:#FFFFFF;color:#3C3A34}
|
||||
/* windows — title bar made visible: darker plate, ink name, clear border; front = red top edge + shadow */
|
||||
.win-frame{background:#FFFFFF;border:1px solid #B9B8AE;display:flex;flex-direction:column;min-width:0;box-shadow:0 1px 2px rgba(35,34,30,0.12)}
|
||||
.win-frame.front{border-color:#767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
|
||||
.stacked .win-frame{position:absolute}
|
||||
.w-title{display:flex;align-items:center;gap:8px;height:28px;padding:0 6px 0 8px;background:#E4E3DC;border-bottom:1px solid #C9C6BC;font-size:11px;color:#3C3A34}
|
||||
.front .w-title{background:#FFFFFF;color:#23221E;border-top:3px solid #C8102E;height:31px}
|
||||
.w-title .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#23221E;font-weight:700}
|
||||
.w-title .app::after{content:"/";margin-left:8px;color:#6B6A64}
|
||||
.w-title .name{font-family:Fraunces,Georgia,serif;font-weight:600;font-size:12px;color:#3C3A34}
|
||||
.front .w-title .name{color:#23221E;font-weight:700}
|
||||
.w-title .new{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#23221E;background:#E4E3DC;padding:0 6px;line-height:15px}
|
||||
.w-title .ctl{display:flex;gap:8px}
|
||||
.w-title .ctl span{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#23221E;background:transparent;border:0}
|
||||
.w-body{padding:0;overflow:hidden}
|
||||
.w-toolbar{display:flex;align-items:center;gap:8px;padding:5px 8px;background:#FAFAF8;border-bottom:1px solid #E6E5DF}
|
||||
.w-h1{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:13px;letter-spacing:-0.01em;margin:0;color:#23221E}
|
||||
.w-count{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#6B6A64}
|
||||
.w-count b{color:#C8102E;font-weight:600}
|
||||
.w-filters{margin-left:auto;display:flex;gap:4px}
|
||||
.w-chip{padding:1px 7px;font-size:9.5px;font-family:"JetBrains Mono",ui-monospace,monospace;border:1px solid #D7D6CE;background:#FFFFFF;color:#3C3A34}
|
||||
.w-chip.on{border-color:#767368;color:#23221E;font-weight:600}
|
||||
.w-cta{padding:2px 9px;font-size:10px;background:#C8102E;color:#FFFFFF;border:1px solid #C8102E;font-weight:600}
|
||||
table.w-tbl{width:100%;border-collapse:collapse;font-size:10.5px;background:#FFFFFF}
|
||||
.w-tbl th{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;text-align:left;padding:4px 8px;white-space:nowrap;background:#F1F1EC;color:#23221E;border-bottom:1px solid #D7D6CE;border-right:1px solid #E6E5DF}
|
||||
.w-tbl td{padding:4px 8px;border-bottom:1px solid #E6E5DF;border-right:1px solid #E6E5DF;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
|
||||
.w-tbl td:last-child,.w-tbl th:last-child{border-right:0}
|
||||
.w-tbl tr:nth-child(even) td{background:#FAFAF8}
|
||||
.w-tbl td.num{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;text-align:right}
|
||||
.w-tbl th.num{text-align:right}
|
||||
.w-tbl td.ref{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.03em}
|
||||
.w-tbl tr.sel td{background:#F7F7F3;box-shadow:inset 3px 0 0 #C8102E}
|
||||
.w-state{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.07em;text-transform:uppercase;padding:1px 5px;background:#F1F1EC;color:#23221E}
|
||||
.rec{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:0}
|
||||
.rec .col{padding:6px 10px 8px;border-right:1px solid #E6E5DF}
|
||||
.rec .col:last-child{border-right:0}
|
||||
.rec h4{margin:4px 0 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;font-weight:600;border-bottom:1px solid #E6E5DF;padding-bottom:3px}
|
||||
.kv{display:grid;grid-template-columns:96px 1fr;gap:2px 8px;font-size:10.5px;margin:0}
|
||||
.kv dt{color:#767368}
|
||||
.kv dd{margin:0;color:#23221E}
|
||||
.kv dd.mono{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;font-variant-numeric:tabular-nums}
|
||||
.rec-actions{display:flex;gap:6px;padding:6px 10px;border-top:1px solid #E6E5DF;background:#FAFAF8}
|
||||
.rec-actions span{padding:2px 9px;font-size:10px;border:1px solid #C7C6BD;background:#FFFFFF;color:#23221E}
|
||||
.rec-actions span.primary{color:#FFFFFF;background:#C8102E;border:1px solid #C8102E;font-weight:600}
|
||||
.steps{display:flex;gap:0;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;border-bottom:1px solid #E6E5DF}
|
||||
.steps span{padding:3px 8px;border-right:1px solid #E6E5DF;color:#6B6A64}
|
||||
.steps span.done{color:#767368}
|
||||
.steps span.now{color:#C8102E;font-weight:700;box-shadow:inset 0 -2px 0 #C8102E}
|
||||
.rep{padding:6px 10px}
|
||||
.rep .row{display:grid;grid-template-columns:150px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid #E6E5DF;font-size:10.5px}
|
||||
.rep .row:last-child{border-bottom:0}
|
||||
.rep .name{color:#23221E;font-weight:600}
|
||||
.rep .name small{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.08em;text-transform:uppercase;color:#6B6A64;font-weight:400}
|
||||
.rep .hl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;font-variant-numeric:tabular-nums}
|
||||
/* title-bar option closeups */
|
||||
.opt{width:600px;padding:16px;background:#D4D0C6}
|
||||
.opt .cap{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;margin:0 0 8px}
|
||||
.opt .win-frame{margin-bottom:14px}
|
||||
.opt .win-frame:last-child{margin-bottom:0}
|
||||
/* Option B: ink strip */
|
||||
.optB .front .w-title{background:#3C3A34;color:#fff;border-top:0;height:28px;border-bottom:1px solid #23221E}
|
||||
.optB .front .w-title .name{color:#fff}
|
||||
.optB .front .w-title .app{color:#fff}
|
||||
.optB .front .w-title .app::after{color:rgba(255,255,255,0.35)}
|
||||
.optB .front .w-title .ctl span{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.6)}
|
||||
.optB .front .w-title .new{color:#fff;border-color:rgba(255,255,255,0.7)}
|
||||
/* Option C: ruled */
|
||||
.optC .w-title{background:#FFFFFF;border-bottom:2px solid #23221E;height:28px}
|
||||
.optC .front .w-title{background:#FFFFFF;border-top:3px solid #C8102E;height:31px;border-bottom:2px solid #23221E}
|
||||
.optC .win-frame{border-color:#23221E}
|
||||
|
||||
|
||||
/* ---- MMD PDF: the parts the shell system has no component for yet ------
|
||||
Named for adoption into the design system, not invented per screen.
|
||||
Every colour below is a red-plate value; nothing new is introduced. */
|
||||
.pdf{width:800px;height:600px;display:flex;flex-direction:column;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26);overflow:hidden}
|
||||
|
||||
/* tool strip */
|
||||
.pdf-tools{display:flex;align-items:stretch;background:#F1F1EC;border-bottom:1px solid #D7D6CE}
|
||||
.pdf-t{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:44px;color:#23221E;flex:none}
|
||||
.pdf-t .lb{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:7.5px;letter-spacing:0.02em;text-transform:uppercase;line-height:1;white-space:nowrap}
|
||||
.pdf-t.on{background:#C8102E;color:#FFFFFF}
|
||||
.pdf-t.dim{color:#A9A69C}
|
||||
.pdf-rule{width:1px;background:#D7D6CE;margin:6px 3px;flex:none}
|
||||
.pdf-end{margin-left:auto;display:flex;align-items:center;gap:5px;padding:0 6px;flex:none}
|
||||
.pdf-zoom{display:flex;align-items:center;border:1px solid #C7C6BD;background:#FFFFFF}
|
||||
.pdf-zoom b{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;font-weight:500;color:#23221E;padding:0 7px;line-height:19px}
|
||||
.pdf-zoom i{width:19px;line-height:19px;text-align:center;font-style:normal;font-size:11px;color:#23221E;border-left:1px solid #C7C6BD}
|
||||
.pdf-over{width:26px;display:flex;align-items:center;justify-content:center;color:#23221E;border-left:1px solid #D7D6CE;flex:none}
|
||||
|
||||
/* icon-only strip (direction B) */
|
||||
.pdf-tools--icons .pdf-t{width:28px}
|
||||
.pdf-tools--icons .pdf-t .lb{display:none}
|
||||
/* tab strip (direction C) */
|
||||
.pdf-tabs{display:flex;align-items:stretch;background:#E4E3DC;border-bottom:1px solid #D7D6CE;padding:0 4px}
|
||||
.pdf-tab{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:0 11px;line-height:21px}
|
||||
.pdf-tab.on{background:#FFFFFF;color:#23221E;font-weight:600;border-top:2px solid #C8102E;line-height:19px}
|
||||
|
||||
/* context bar — only present while a tool is armed */
|
||||
.pdf-ctx{display:flex;align-items:center;gap:9px;height:26px;padding:0 9px;background:#FAFAF8;border-bottom:1px solid #E6E5DF;font-size:10px;color:#3C3A34}
|
||||
.pdf-ctx .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.pdf-sw{display:flex;gap:3px}
|
||||
.pdf-sw i{width:12px;height:12px;display:block;border:1px solid #C7C6BD}
|
||||
.pdf-sw i.on{outline:1px solid #23221E;outline-offset:1px}
|
||||
.pdf-pick{border:1px solid #C7C6BD;background:#FFFFFF;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;padding:1px 6px}
|
||||
|
||||
/* work area */
|
||||
.pdf-work{flex:1;display:flex;min-height:0}
|
||||
.pdf-strip{width:26px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:7px}
|
||||
.pdf-strip .pn{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;color:#23221E;line-height:1.25;text-align:center}
|
||||
.pdf-rail{width:88px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;overflow:hidden}
|
||||
.pdf-rail-h{display:flex;gap:7px;padding:5px 7px 3px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.pdf-rail-h .on{color:#23221E;font-weight:700}
|
||||
.pdf-pg{margin:0 7px 5px;background:#FFFFFF;border:1px solid #C7C6BD}
|
||||
.pdf-pg.here{box-shadow:inset 3px 0 0 #C8102E}
|
||||
.pdf-pg .mini{height:64px;padding:5px 6px;overflow:hidden}
|
||||
.pdf-pg .mini i{display:block;background:#C9C7C0;height:2px;margin-bottom:3px}
|
||||
.pdf-pg .mini i.t{background:#23221E;height:3px;width:74%;margin-bottom:5px}
|
||||
.pdf-pg .mini i.s{width:52%}
|
||||
.pdf-pg .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;text-align:center;color:#3C3A34;line-height:14px;border-top:1px solid #E6E5DF}
|
||||
.pdf-canvas{flex:1;background:#F3F3F0;display:flex;justify-content:center;overflow:hidden;padding:9px 0 0}
|
||||
.pdf-page{width:100%;max-width:560px;background:#FFFFFF;border:1px solid #C7C6BD;padding:28px 34px}
|
||||
|
||||
/* status */
|
||||
.pdf-status{display:flex;align-items:center;gap:12px;height:20px;padding:0 8px;background:#F1F1EC;border-top:1px solid #D7D6CE;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;color:#3C3A34;flex:none}
|
||||
.pdf-status .sep{width:1px;height:11px;background:#C7C6BD}
|
||||
.pdf-status .r{margin-left:auto;color:#6B6A64}
|
||||
.pdf-status .warn{color:#C8102E}
|
||||
|
||||
/* document body */
|
||||
.pdf-h{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:14px;color:#23221E;margin:0 0 13px}
|
||||
.pdf-p{font-size:10.5px;color:#3C3A34;line-height:1.9;margin:0}
|
||||
.pdf-live{display:inline-block;padding:0 4px;border:1px solid #C8102E;background:#FBEEF0;color:#23221E}
|
||||
|
||||
/* dialog */
|
||||
.pdf-behind{position:absolute;inset:0}
|
||||
.pdf-behind::after{content:"";position:absolute;inset:0;background:rgba(35,34,30,0.28)}
|
||||
.pdf-over-scrim{position:relative;width:800px;height:600px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.pdf-over-scrim .pdf-dlg{position:relative;z-index:2}
|
||||
.pdf-dlg{width:404px;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
|
||||
.pdf-dlg-t{display:flex;align-items:center;height:26px;padding:0 10px;background:#FFFFFF;color:#23221E;border-bottom:2px solid #23221E;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:11.5px}
|
||||
.pdf-dlg-b{padding:13px 15px;display:flex;flex-direction:column;gap:10px;font-size:10.5px;color:#3C3A34}
|
||||
.pdf-dlg-a{display:flex;justify-content:flex-end;gap:6px;padding:0 15px 13px}
|
||||
.pdf-kv{display:grid;grid-template-columns:82px 1fr;gap:4px 10px;align-items:baseline}
|
||||
.pdf-kv .k{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.09em;text-transform:uppercase;color:#767368}
|
||||
.pdf-in{border:1px solid #C7C6BD;background:#FFFFFF;padding:3px 7px;font-size:10.5px;color:#23221E;display:flex;align-items:center}
|
||||
.pdf-in .cv{margin-left:auto;font-size:8px;color:#767368}
|
||||
|
||||
/* direction board */
|
||||
.board{width:800px;background:#F3F3F0;padding:11px;display:flex;flex-direction:column;gap:11px}
|
||||
.board h3{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px;color:#23221E;margin:0}
|
||||
.board .why{font-size:10px;color:#3C3A34;margin:0}
|
||||
.board .cost{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;color:#767368;margin:0}
|
||||
.board .card{background:#FFFFFF;border:1px solid #C7C6BD;padding:9px 10px;display:flex;flex-direction:column;gap:6px}
|
||||
.board .strip{border:1px solid #D7D6CE}
|
||||
|
||||
</style>
|
||||
</helmet>
|
||||
<div class="board">
|
||||
<div class="card">
|
||||
<h3>A — one row, named tools</h3>
|
||||
<div class="strip"><div class="pdf-tools" style="height:40px"><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M2 6h6l2 2h8v9H2z"/></svg><span class="lb">Open</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3"/><path d="M6 17v-5h8v5"/></svg><span class="lb">Save</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 8V3h8v5"/><path d="M3 8h14v6h-3v4H6v-4H3z"/></svg><span class="lb">Print</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5V3h4"/><path d="M17 5V3h-4"/><path d="M3 15v2h4"/><path d="M17 15v2h-4"/><path d="M2 10h16"/></svg><span class="lb">Scan</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 6V3h3"/><path d="M17 6V3h-3"/><path d="M3 14v3h3"/><path d="M17 14v3h-3"/><path d="M6 9h8"/><path d="M6 12h5"/></svg><span class="lb">OCR</span></div><span class="pdf-rule"></span><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg><span class="lb">Select</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg><span class="lb">Text</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg><span class="lb">Mark</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg><span class="lb">Draw</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg><span class="lb">Shape</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg><span class="lb">Sign</span></div><span class="pdf-rule"></span><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 3h8v14H4z"/><path d="M12 6h4v11H8"/></svg><span class="lb">Pages</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 13l4 4"/><path d="M4 9a5 5 0 1010 0 5 5 0 10-10 0"/></svg><span class="lb">Search</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M7 6L3 10l4 4"/><path d="M3 10h9a4 4 0 010 8"/></svg><span class="lb">Undo</span></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>−</i><i>+</i></span></div><div class="pdf-over"><svg width="14" height="14" viewBox="0 0 14 14" fill="#23221E"><circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/><circle cx="11" cy="7" r="1.2"/></svg></div></div></div>
|
||||
<p class="why">Fourteen tools carry their name. Everything else — stamp, image, crop, merge, flatten, redo — lives behind the three dots on the right.</p>
|
||||
<p class="cost">40px of chrome · 512px left for the page · 16 of 30 tools hidden</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>B — icons only, nothing hidden</h3>
|
||||
<div class="strip"><div class="pdf-tools pdf-tools--icons" style="height:30px"><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M2 6h6l2 2h8v9H2z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3"/><path d="M6 17v-5h8v5"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 8V3h8v5"/><path d="M3 8h14v6h-3v4H6v-4H3z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5V3h4"/><path d="M17 5V3h-4"/><path d="M3 15v2h4"/><path d="M17 15v2h-4"/><path d="M2 10h16"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 6V3h3"/><path d="M17 6V3h-3"/><path d="M3 14v3h3"/><path d="M17 14v3h-3"/><path d="M6 9h8"/><path d="M6 12h5"/></svg></div><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 16h8"/><path d="M7 13h6l-1-5a2 2 0 10-4 0z"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 4h14v12H3z"/><path d="M3 13l4-4 3 3 3-3 4 4"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 2v12h12"/><path d="M2 6h12v12"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5h6v4H3z"/><path d="M11 11h6v4h-6z"/><path d="M6 9v3h5"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 3h8v7H6z"/><path d="M3 13h14"/><path d="M3 16h14"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 3h8v14H4z"/><path d="M12 6h4v11H8"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 13l4 4"/><path d="M4 9a5 5 0 1010 0 5 5 0 10-10 0"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M7 6L3 10l4 4"/><path d="M3 10h9a4 4 0 010 8"/></svg></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 6l4 4-4 4"/><path d="M17 10H8a4 4 0 000 8"/></svg></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>−</i><i>+</i></span></div></div></div>
|
||||
<p class="why">All thirty fit, name on hover. Fastest once someone knows the icons; a plant clerk opening it for the first time has to guess.</p>
|
||||
<p class="cost">30px of chrome · 522px left for the page · nothing hidden</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>C — three tabs, named tools, nothing hidden</h3>
|
||||
<div class="strip"><div class="pdf-tabs"><span class="pdf-tab">Document</span><span class="pdf-tab on">Edit</span><span class="pdf-tab">Pages</span></div><div class="pdf-tools" style="height:34px"><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg><span class="lb">Select</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg><span class="lb">Text</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg><span class="lb">Mark</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg><span class="lb">Draw</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg><span class="lb">Shape</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg><span class="lb">Sign</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 16h8"/><path d="M7 13h6l-1-5a2 2 0 10-4 0z"/></svg><span class="lb">Stamp</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 4h14v12H3z"/><path d="M3 13l4-4 3 3 3-3 4 4"/></svg><span class="lb">Image</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 2v12h12"/><path d="M2 6h12v12"/></svg><span class="lb">Crop</span></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>−</i><i>+</i></span></div></div></div>
|
||||
<p class="why">Document, Edit, Pages. Every tool keeps its name and every tool is reachable, at the price of one click to change tab and a second bar of chrome.</p>
|
||||
<p class="cost">56px of chrome · 496px left for the page · nothing hidden, one click deeper</p>
|
||||
</div>
|
||||
</div></x-dc>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,205 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
||||
<style>
|
||||
|
||||
body{margin:0;background:#F3F3F0;font-family:"PP Neue Machina","Neue Machina",system-ui,-apple-system,"Segoe UI",sans-serif;color:#3C3A34;-webkit-font-smoothing:antialiased}
|
||||
a{color:#C8102E} a:hover{color:#8E0B20}
|
||||
*{box-sizing:border-box}
|
||||
.root{padding:16px;background:#F3F3F0}
|
||||
.shell{width:1180px;display:flex;flex-direction:column;font-size:11.5px;background:#FFFFFF;border:1px solid #C7C6BD}
|
||||
.s-title{display:flex;align-items:center;gap:10px;padding:0 10px;height:30px;background:#C8102E;color:#fff;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px}
|
||||
.s-title .mmd-logo{height:19px;width:auto;display:block;flex:none;fill:#fff}
|
||||
.spring{flex:1}
|
||||
.s-who{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.07em;text-transform:uppercase;color:rgba(255,255,255,0.85)}
|
||||
.s-signout{font-size:10px;padding:1px 0;color:#fff;text-decoration:underline;text-underline-offset:2px}
|
||||
.s-bar{display:flex;align-items:stretch;gap:2px;padding:0 5px;background:#F7F7F3;border-bottom:1px solid #D7D6CE}
|
||||
.s-item{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;white-space:nowrap;border:1px solid transparent;color:#3C3A34}
|
||||
.s-item .cv{font-size:8px;opacity:0.5}
|
||||
.s-item .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;padding:0 4px;border:1px solid #23221E;color:#23221E;background:#FFFFFF;line-height:13px}
|
||||
.s-item.on{background:#FFFFFF;border-color:#D7D6CE;border-top:2px solid #C8102E;border-bottom-color:#FFFFFF;margin-bottom:-1px;font-weight:700;color:#23221E}
|
||||
.s-item.on .n{border-color:#23221E;color:#23221E}
|
||||
.s-tools{margin-left:auto;display:flex;align-items:center;gap:6px;padding-right:4px}
|
||||
.s-search{width:168px;padding:2px 7px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
|
||||
.s-btn{padding:2px 9px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
|
||||
.s-desk{position:relative;background:#D4D0C6;overflow:hidden}
|
||||
.s-desk.tiled{display:grid;grid-template-columns:1.35fr 1fr;grid-template-rows:auto auto;gap:10px;padding:10px;align-items:start}
|
||||
.s-desk.stacked{height:492px}
|
||||
.s-desk.oneup{padding:10px}
|
||||
.s-task{display:flex;align-items:center;gap:10px;padding:4px 8px;font-size:10.5px;background:#F1F1EC;border-top:1px solid #D7D6CE}
|
||||
.s-task .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.s-group{display:inline-flex;align-items:stretch;border:1px solid #D7D6CE;background:#FFFFFF}
|
||||
.s-group .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:2px 7px;border-right:1px solid #D7D6CE;display:flex;align-items:center;background:#F7F7F3}
|
||||
.s-group .win{padding:2px 8px;font-size:10px;color:#23221E;border-right:1px solid #D7D6CE;display:flex;align-items:center;gap:5px;background:#F1F1EC}
|
||||
.s-group .win:last-child{border-right:0}
|
||||
.s-group .win.on{box-shadow:inset 3px 0 0 #C8102E;font-weight:700;background:#FFFFFF}
|
||||
.s-group .win.min{color:#3C3A34;background:#FFFFFF;outline:1px dashed #6B6A64;outline-offset:-3px}
|
||||
|
||||
.s-task .tile{font-size:10px;padding:1px 8px;border:1px solid #C7C6BD;background:#FFFFFF;color:#3C3A34}
|
||||
/* windows — title bar made visible: darker plate, ink name, clear border; front = red top edge + shadow */
|
||||
.win-frame{background:#FFFFFF;border:1px solid #B9B8AE;display:flex;flex-direction:column;min-width:0;box-shadow:0 1px 2px rgba(35,34,30,0.12)}
|
||||
.win-frame.front{border-color:#767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
|
||||
.stacked .win-frame{position:absolute}
|
||||
.w-title{display:flex;align-items:center;gap:8px;height:28px;padding:0 6px 0 8px;background:#E4E3DC;border-bottom:1px solid #C9C6BC;font-size:11px;color:#3C3A34}
|
||||
.front .w-title{background:#FFFFFF;color:#23221E;border-top:3px solid #C8102E;height:31px}
|
||||
.w-title .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#23221E;font-weight:700}
|
||||
.w-title .app::after{content:"/";margin-left:8px;color:#6B6A64}
|
||||
.w-title .name{font-family:Fraunces,Georgia,serif;font-weight:600;font-size:12px;color:#3C3A34}
|
||||
.front .w-title .name{color:#23221E;font-weight:700}
|
||||
.w-title .new{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#23221E;background:#E4E3DC;padding:0 6px;line-height:15px}
|
||||
.w-title .ctl{display:flex;gap:8px}
|
||||
.w-title .ctl span{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#23221E;background:transparent;border:0}
|
||||
.w-body{padding:0;overflow:hidden}
|
||||
.w-toolbar{display:flex;align-items:center;gap:8px;padding:5px 8px;background:#FAFAF8;border-bottom:1px solid #E6E5DF}
|
||||
.w-h1{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:13px;letter-spacing:-0.01em;margin:0;color:#23221E}
|
||||
.w-count{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#6B6A64}
|
||||
.w-count b{color:#C8102E;font-weight:600}
|
||||
.w-filters{margin-left:auto;display:flex;gap:4px}
|
||||
.w-chip{padding:1px 7px;font-size:9.5px;font-family:"JetBrains Mono",ui-monospace,monospace;border:1px solid #D7D6CE;background:#FFFFFF;color:#3C3A34}
|
||||
.w-chip.on{border-color:#767368;color:#23221E;font-weight:600}
|
||||
.w-cta{padding:2px 9px;font-size:10px;background:#C8102E;color:#FFFFFF;border:1px solid #C8102E;font-weight:600}
|
||||
table.w-tbl{width:100%;border-collapse:collapse;font-size:10.5px;background:#FFFFFF}
|
||||
.w-tbl th{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;text-align:left;padding:4px 8px;white-space:nowrap;background:#F1F1EC;color:#23221E;border-bottom:1px solid #D7D6CE;border-right:1px solid #E6E5DF}
|
||||
.w-tbl td{padding:4px 8px;border-bottom:1px solid #E6E5DF;border-right:1px solid #E6E5DF;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
|
||||
.w-tbl td:last-child,.w-tbl th:last-child{border-right:0}
|
||||
.w-tbl tr:nth-child(even) td{background:#FAFAF8}
|
||||
.w-tbl td.num{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;text-align:right}
|
||||
.w-tbl th.num{text-align:right}
|
||||
.w-tbl td.ref{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.03em}
|
||||
.w-tbl tr.sel td{background:#F7F7F3;box-shadow:inset 3px 0 0 #C8102E}
|
||||
.w-state{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.07em;text-transform:uppercase;padding:1px 5px;background:#F1F1EC;color:#23221E}
|
||||
.rec{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:0}
|
||||
.rec .col{padding:6px 10px 8px;border-right:1px solid #E6E5DF}
|
||||
.rec .col:last-child{border-right:0}
|
||||
.rec h4{margin:4px 0 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;font-weight:600;border-bottom:1px solid #E6E5DF;padding-bottom:3px}
|
||||
.kv{display:grid;grid-template-columns:96px 1fr;gap:2px 8px;font-size:10.5px;margin:0}
|
||||
.kv dt{color:#767368}
|
||||
.kv dd{margin:0;color:#23221E}
|
||||
.kv dd.mono{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;font-variant-numeric:tabular-nums}
|
||||
.rec-actions{display:flex;gap:6px;padding:6px 10px;border-top:1px solid #E6E5DF;background:#FAFAF8}
|
||||
.rec-actions span{padding:2px 9px;font-size:10px;border:1px solid #C7C6BD;background:#FFFFFF;color:#23221E}
|
||||
.rec-actions span.primary{color:#FFFFFF;background:#C8102E;border:1px solid #C8102E;font-weight:600}
|
||||
.steps{display:flex;gap:0;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;border-bottom:1px solid #E6E5DF}
|
||||
.steps span{padding:3px 8px;border-right:1px solid #E6E5DF;color:#6B6A64}
|
||||
.steps span.done{color:#767368}
|
||||
.steps span.now{color:#C8102E;font-weight:700;box-shadow:inset 0 -2px 0 #C8102E}
|
||||
.rep{padding:6px 10px}
|
||||
.rep .row{display:grid;grid-template-columns:150px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid #E6E5DF;font-size:10.5px}
|
||||
.rep .row:last-child{border-bottom:0}
|
||||
.rep .name{color:#23221E;font-weight:600}
|
||||
.rep .name small{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.08em;text-transform:uppercase;color:#6B6A64;font-weight:400}
|
||||
.rep .hl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;font-variant-numeric:tabular-nums}
|
||||
/* title-bar option closeups */
|
||||
.opt{width:600px;padding:16px;background:#D4D0C6}
|
||||
.opt .cap{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;margin:0 0 8px}
|
||||
.opt .win-frame{margin-bottom:14px}
|
||||
.opt .win-frame:last-child{margin-bottom:0}
|
||||
/* Option B: ink strip */
|
||||
.optB .front .w-title{background:#3C3A34;color:#fff;border-top:0;height:28px;border-bottom:1px solid #23221E}
|
||||
.optB .front .w-title .name{color:#fff}
|
||||
.optB .front .w-title .app{color:#fff}
|
||||
.optB .front .w-title .app::after{color:rgba(255,255,255,0.35)}
|
||||
.optB .front .w-title .ctl span{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.6)}
|
||||
.optB .front .w-title .new{color:#fff;border-color:rgba(255,255,255,0.7)}
|
||||
/* Option C: ruled */
|
||||
.optC .w-title{background:#FFFFFF;border-bottom:2px solid #23221E;height:28px}
|
||||
.optC .front .w-title{background:#FFFFFF;border-top:3px solid #C8102E;height:31px;border-bottom:2px solid #23221E}
|
||||
.optC .win-frame{border-color:#23221E}
|
||||
|
||||
|
||||
/* ---- MMD PDF: the parts the shell system has no component for yet ------
|
||||
Named for adoption into the design system, not invented per screen.
|
||||
Every colour below is a red-plate value; nothing new is introduced. */
|
||||
.pdf{width:800px;height:600px;display:flex;flex-direction:column;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26);overflow:hidden}
|
||||
|
||||
/* tool strip */
|
||||
.pdf-tools{display:flex;align-items:stretch;background:#F1F1EC;border-bottom:1px solid #D7D6CE}
|
||||
.pdf-t{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:44px;color:#23221E;flex:none}
|
||||
.pdf-t .lb{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:7.5px;letter-spacing:0.02em;text-transform:uppercase;line-height:1;white-space:nowrap}
|
||||
.pdf-t.on{background:#C8102E;color:#FFFFFF}
|
||||
.pdf-t.dim{color:#A9A69C}
|
||||
.pdf-rule{width:1px;background:#D7D6CE;margin:6px 3px;flex:none}
|
||||
.pdf-end{margin-left:auto;display:flex;align-items:center;gap:5px;padding:0 6px;flex:none}
|
||||
.pdf-zoom{display:flex;align-items:center;border:1px solid #C7C6BD;background:#FFFFFF}
|
||||
.pdf-zoom b{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;font-weight:500;color:#23221E;padding:0 7px;line-height:19px}
|
||||
.pdf-zoom i{width:19px;line-height:19px;text-align:center;font-style:normal;font-size:11px;color:#23221E;border-left:1px solid #C7C6BD}
|
||||
.pdf-over{width:26px;display:flex;align-items:center;justify-content:center;color:#23221E;border-left:1px solid #D7D6CE;flex:none}
|
||||
|
||||
/* icon-only strip (direction B) */
|
||||
.pdf-tools--icons .pdf-t{width:28px}
|
||||
.pdf-tools--icons .pdf-t .lb{display:none}
|
||||
/* tab strip (direction C) */
|
||||
.pdf-tabs{display:flex;align-items:stretch;background:#E4E3DC;border-bottom:1px solid #D7D6CE;padding:0 4px}
|
||||
.pdf-tab{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:0 11px;line-height:21px}
|
||||
.pdf-tab.on{background:#FFFFFF;color:#23221E;font-weight:600;border-top:2px solid #C8102E;line-height:19px}
|
||||
|
||||
/* context bar — only present while a tool is armed */
|
||||
.pdf-ctx{display:flex;align-items:center;gap:9px;height:26px;padding:0 9px;background:#FAFAF8;border-bottom:1px solid #E6E5DF;font-size:10px;color:#3C3A34}
|
||||
.pdf-ctx .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.pdf-sw{display:flex;gap:3px}
|
||||
.pdf-sw i{width:12px;height:12px;display:block;border:1px solid #C7C6BD}
|
||||
.pdf-sw i.on{outline:1px solid #23221E;outline-offset:1px}
|
||||
.pdf-pick{border:1px solid #C7C6BD;background:#FFFFFF;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;padding:1px 6px}
|
||||
|
||||
/* work area */
|
||||
.pdf-work{flex:1;display:flex;min-height:0}
|
||||
.pdf-strip{width:26px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:7px}
|
||||
.pdf-strip .pn{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;color:#23221E;line-height:1.25;text-align:center}
|
||||
.pdf-rail{width:88px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;overflow:hidden}
|
||||
.pdf-rail-h{display:flex;gap:7px;padding:5px 7px 3px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.pdf-rail-h .on{color:#23221E;font-weight:700}
|
||||
.pdf-pg{margin:0 7px 5px;background:#FFFFFF;border:1px solid #C7C6BD}
|
||||
.pdf-pg.here{box-shadow:inset 3px 0 0 #C8102E}
|
||||
.pdf-pg .mini{height:64px;padding:5px 6px;overflow:hidden}
|
||||
.pdf-pg .mini i{display:block;background:#C9C7C0;height:2px;margin-bottom:3px}
|
||||
.pdf-pg .mini i.t{background:#23221E;height:3px;width:74%;margin-bottom:5px}
|
||||
.pdf-pg .mini i.s{width:52%}
|
||||
.pdf-pg .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8px;text-align:center;color:#3C3A34;line-height:14px;border-top:1px solid #E6E5DF}
|
||||
.pdf-canvas{flex:1;background:#F3F3F0;display:flex;justify-content:center;overflow:hidden;padding:9px 0 0}
|
||||
.pdf-page{width:100%;max-width:560px;background:#FFFFFF;border:1px solid #C7C6BD;padding:28px 34px}
|
||||
|
||||
/* status */
|
||||
.pdf-status{display:flex;align-items:center;gap:12px;height:20px;padding:0 8px;background:#F1F1EC;border-top:1px solid #D7D6CE;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;color:#3C3A34;flex:none}
|
||||
.pdf-status .sep{width:1px;height:11px;background:#C7C6BD}
|
||||
.pdf-status .r{margin-left:auto;color:#6B6A64}
|
||||
.pdf-status .warn{color:#C8102E}
|
||||
|
||||
/* document body */
|
||||
.pdf-h{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:14px;color:#23221E;margin:0 0 13px}
|
||||
.pdf-p{font-size:10.5px;color:#3C3A34;line-height:1.9;margin:0}
|
||||
.pdf-live{display:inline-block;padding:0 4px;border:1px solid #C8102E;background:#FBEEF0;color:#23221E}
|
||||
|
||||
/* dialog */
|
||||
.pdf-behind{position:absolute;inset:0}
|
||||
.pdf-behind::after{content:"";position:absolute;inset:0;background:rgba(35,34,30,0.28)}
|
||||
.pdf-over-scrim{position:relative;width:800px;height:600px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.pdf-over-scrim .pdf-dlg{position:relative;z-index:2}
|
||||
.pdf-dlg{width:404px;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
|
||||
.pdf-dlg-t{display:flex;align-items:center;height:26px;padding:0 10px;background:#FFFFFF;color:#23221E;border-bottom:2px solid #23221E;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:11.5px}
|
||||
.pdf-dlg-b{padding:13px 15px;display:flex;flex-direction:column;gap:10px;font-size:10.5px;color:#3C3A34}
|
||||
.pdf-dlg-a{display:flex;justify-content:flex-end;gap:6px;padding:0 15px 13px}
|
||||
.pdf-kv{display:grid;grid-template-columns:82px 1fr;gap:4px 10px;align-items:baseline}
|
||||
.pdf-kv .k{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.09em;text-transform:uppercase;color:#767368}
|
||||
.pdf-in{border:1px solid #C7C6BD;background:#FFFFFF;padding:3px 7px;font-size:10.5px;color:#23221E;display:flex;align-items:center}
|
||||
.pdf-in .cv{margin-left:auto;font-size:8px;color:#767368}
|
||||
|
||||
/* direction board */
|
||||
.board{width:800px;background:#F3F3F0;padding:11px;display:flex;flex-direction:column;gap:11px}
|
||||
.board h3{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px;color:#23221E;margin:0}
|
||||
.board .why{font-size:10px;color:#3C3A34;margin:0}
|
||||
.board .cost{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;color:#767368;margin:0}
|
||||
.board .card{background:#FFFFFF;border:1px solid #C7C6BD;padding:9px 10px;display:flex;flex-direction:column;gap:6px}
|
||||
.board .strip{border:1px solid #D7D6CE}
|
||||
|
||||
</style>
|
||||
</helmet>
|
||||
<div class="pdf"><div class="w-title"><img class="mmd-logo" src="mmd-logo-red.svg" alt="MMD" style="height:13px"><span class="app">MMD PDF</span><span class="name">BON-2026-0847 delivery note.pdf</span><span class="spring"></span><span class="ctl"><span>−</span><span>◻</span><span>×</span></span></div><div class="pdf-tools" style="height:40px"><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M2 6h6l2 2h8v9H2z"/></svg><span class="lb">Open</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3"/><path d="M6 17v-5h8v5"/></svg><span class="lb">Save</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M6 8V3h8v5"/><path d="M3 8h14v6h-3v4H6v-4H3z"/></svg><span class="lb">Print</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 5V3h4"/><path d="M17 5V3h-4"/><path d="M3 15v2h4"/><path d="M17 15v2h-4"/><path d="M2 10h16"/></svg><span class="lb">Scan</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 6V3h3"/><path d="M17 6V3h-3"/><path d="M3 14v3h3"/><path d="M17 14v3h-3"/><path d="M6 9h8"/><path d="M6 12h5"/></svg><span class="lb">OCR</span></div><span class="pdf-rule"></span><div class="pdf-t on"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#FFFFFF" stroke-width="1.35" stroke-linecap="square"><path d="M5 3l11 8-5 1 3 5-2 1-3-5-4 3z"/></svg><span class="lb">Select</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 5h12"/><path d="M10 5v11"/></svg><span class="lb">Text</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 15l3-9h2l3 9"/><path d="M6 12h5"/><path d="M14 15h3"/></svg><span class="lb">Mark</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 17l3-1 9-9-2-2-9 9z"/></svg><span class="lb">Draw</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 8h8v8H3z"/><path d="M11 4h6v6h-6z"/></svg><span class="lb">Shape</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M3 14c3-6 5 3 7-2s4 2 7-3"/><path d="M3 17h14"/></svg><span class="lb">Sign</span></div><span class="pdf-rule"></span><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M4 3h8v14H4z"/><path d="M12 6h4v11H8"/></svg><span class="lb">Pages</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M13 13l4 4"/><path d="M4 9a5 5 0 1010 0 5 5 0 10-10 0"/></svg><span class="lb">Search</span></div><div class="pdf-t"><svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="#23221E" stroke-width="1.35" stroke-linecap="square"><path d="M7 6L3 10l4 4"/><path d="M3 10h9a4 4 0 010 8"/></svg><span class="lb">Undo</span></div><div class="pdf-end"><span class="pdf-zoom"><b>FIT WIDTH</b><i>−</i><i>+</i></span></div><div class="pdf-over"><svg width="14" height="14" viewBox="0 0 14 14" fill="#23221E"><circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/><circle cx="11" cy="7" r="1.2"/></svg></div></div><div class="pdf-work"><div class="pdf-strip"><svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="#23221E" stroke-width="1.3"><path d="M4 2l4 4-4 4"/></svg><span class="pn">1</span></div><div class="pdf-canvas"><div class="pdf-page"><h1 class="pdf-h">MMD STEEL GROUP — DELIVERY NOTE</h1><p class="pdf-p">Supplier: Fabrimetal SARL<br>Reference: BON-2026-0847<br>Quantity delivered: 12 tonnes<br>Vehicle: DK-4471-AB · Driver: M. Sow<br><br>Received by: ______________________<br><br>Dakar plant · corridor freight · weighbridge ticket WB-4471</p></div></div></div><div class="pdf-status"><span class="">Ready</span><span class="r">1 OF 1 · FIT WIDTH · 100%</span></div></div></x-dc>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,413 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MMD PDF — design pages for the desktop editor at 800x600.
|
||||
|
||||
Stylesheet: the red-plate MMD shell design system, lifted verbatim from its
|
||||
own source (portal-redesign session, canvas/build.mjs). Not retyped.
|
||||
|
||||
The screen budget at 800x600 drives every decision here:
|
||||
600 - 28 title bar - 20 status bar = 552px for toolbar + document.
|
||||
800 - page rail = document width.
|
||||
A PDF editor carries about thirty tools. Thirty labelled tools need ~1400px.
|
||||
Three directions resolve that differently; A/B/C are real alternatives, not
|
||||
cosmetic variants of one answer.
|
||||
"""
|
||||
import json, os
|
||||
|
||||
OUT = os.path.dirname(os.path.abspath(__file__))
|
||||
RP = open(os.path.join(OUT, "redplate.css")).read()
|
||||
W, H = 800, 600
|
||||
|
||||
EXT = """
|
||||
/* ---- MMD PDF: the parts the shell system has no component for yet ------
|
||||
Named for adoption into the design system, not invented per screen.
|
||||
Every colour below is a red-plate value; nothing new is introduced. */
|
||||
.pdf{width:800px;height:600px;display:flex;flex-direction:column;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26);overflow:hidden}
|
||||
|
||||
/* tool strip */
|
||||
.pdf-tools{display:flex;align-items:stretch;background:#F1F1EC;border-bottom:1px solid #D7D6CE}
|
||||
.pdf-t{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;width:44px;color:#23221E;flex:none}
|
||||
.pdf-t .lb{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.01em;text-transform:uppercase;line-height:1;white-space:nowrap}
|
||||
.pdf-t.on{background:#C8102E;color:#FFFFFF}
|
||||
.pdf-t.dim{color:#767368}
|
||||
.pdf-rule{width:1px;background:#D7D6CE;margin:6px 3px;flex:none}
|
||||
.pdf-end{margin-left:auto;display:flex;align-items:center;gap:5px;padding:0 6px;flex:none}
|
||||
.pdf-zoom{display:flex;align-items:center;border:1px solid #C7C6BD;background:#FFFFFF}
|
||||
.pdf-zoom b{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;font-weight:500;color:#23221E;padding:0 7px;line-height:19px}
|
||||
.pdf-zoom i{width:19px;line-height:19px;text-align:center;font-style:normal;font-size:11px;color:#23221E;border-left:1px solid #C7C6BD}
|
||||
.pdf-over{width:26px;display:flex;align-items:center;justify-content:center;color:#23221E;border-left:1px solid #D7D6CE;flex:none}
|
||||
|
||||
/* icon-only strip (direction B) */
|
||||
.pdf-tools--icons .pdf-t{width:28px}
|
||||
.pdf-tools--icons .pdf-t .lb{display:none}
|
||||
/* tab strip (direction C) */
|
||||
.pdf-tabs{display:flex;align-items:stretch;background:#E4E3DC;border-bottom:1px solid #D7D6CE;padding:0 4px}
|
||||
.pdf-tab{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:0 11px;line-height:21px}
|
||||
.pdf-tab.on{background:#FFFFFF;color:#23221E;font-weight:600;border-top:2px solid #C8102E;line-height:19px}
|
||||
|
||||
/* context bar — only present while a tool is armed */
|
||||
.pdf-ctx{display:flex;align-items:center;gap:9px;height:28px;padding:0 9px;background:#FAFAF8;border-bottom:1px solid #E6E5DF;font-size:10px;color:#3C3A34}
|
||||
.pdf-ctx .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.pdf-sw{display:flex;gap:3px}
|
||||
.pdf-sw i{width:12px;height:12px;display:block;border:1px solid #C7C6BD}
|
||||
.pdf-sw i.on{outline:1px solid #23221E;outline-offset:1px}
|
||||
.pdf-pick{border:1px solid #C7C6BD;background:#FFFFFF;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;padding:1px 6px}
|
||||
|
||||
/* work area */
|
||||
.pdf-work{flex:1;display:flex;min-height:0}
|
||||
.pdf-strip{width:26px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:7px}
|
||||
.pdf-strip .pn{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#23221E;line-height:1.25;text-align:center}
|
||||
.pdf-rail{width:88px;flex:none;background:#E4E3DC;border-right:1px solid #D7D6CE;overflow:hidden}
|
||||
.pdf-rail-h{display:flex;gap:8px;padding:6px 7px 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.pdf-rail-h .on{color:#23221E;font-weight:700}
|
||||
.pdf-pg{margin:0 7px 5px;background:#FFFFFF;border:1px solid #C7C6BD}
|
||||
.pdf-pg.here{box-shadow:inset 3px 0 0 #C8102E}
|
||||
.pdf-pg .mini{height:64px;padding:5px 6px;overflow:hidden}
|
||||
.pdf-pg .mini i{display:block;background:#C7C6BD;height:2px;margin-bottom:3px}
|
||||
.pdf-pg .mini i.t{background:#23221E;height:3px;width:74%;margin-bottom:5px}
|
||||
.pdf-pg .mini i.s{width:52%}
|
||||
.pdf-pg .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;text-align:center;color:#3C3A34;line-height:16px;border-top:1px solid #E6E5DF}
|
||||
.pdf-canvas{flex:1;background:#F3F3F0;display:flex;justify-content:center;overflow:hidden;padding:9px 0 0}
|
||||
.pdf-page{width:100%;max-width:560px;background:#FFFFFF;border:1px solid #C7C6BD;padding:28px 34px}
|
||||
|
||||
/* status */
|
||||
.pdf-status{display:flex;align-items:center;gap:12px;height:23px;padding:0 8px;background:#F1F1EC;border-top:1px solid #D7D6CE;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;flex:none}
|
||||
.pdf-status .sep{width:1px;height:11px;background:#C7C6BD}
|
||||
.pdf-status .r{margin-left:auto;color:#6B6A64}
|
||||
.pdf-status .warn{color:#C8102E}
|
||||
|
||||
/* document body */
|
||||
.pdf-h{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:14px;color:#23221E;margin:0 0 13px}
|
||||
.pdf-p{font-size:10.5px;color:#3C3A34;line-height:1.9;margin:0}
|
||||
.pdf-live{display:inline-block;padding:0 4px;border:1px solid #C8102E;background:#FBEEF0;color:#23221E}
|
||||
|
||||
/* dialog */
|
||||
.pdf-behind{position:absolute;inset:0}
|
||||
.pdf-behind::after{content:"";position:absolute;inset:0;background:rgba(35,34,30,0.28)}
|
||||
.pdf-over-scrim{position:relative;width:800px;height:600px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.pdf-over-scrim .pdf-dlg{position:relative;z-index:2}
|
||||
.pdf-dlg{width:404px;background:#FFFFFF;border:1px solid #767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
|
||||
.pdf-dlg-t{display:flex;align-items:center;height:26px;padding:0 10px;background:#FFFFFF;color:#23221E;border-bottom:2px solid #23221E;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:11.5px}
|
||||
.pdf-dlg-b{padding:13px 15px;display:flex;flex-direction:column;gap:10px;font-size:10.5px;color:#3C3A34}
|
||||
.pdf-dlg-a{display:flex;justify-content:flex-end;gap:6px;padding:0 15px 13px}
|
||||
.pdf-kv{display:grid;grid-template-columns:82px 1fr;gap:4px 10px;align-items:baseline}
|
||||
.pdf-kv .k{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.09em;text-transform:uppercase;color:#767368}
|
||||
.pdf-in{border:1px solid #C7C6BD;background:#FFFFFF;padding:3px 7px;font-size:10.5px;color:#23221E;display:flex;align-items:center}
|
||||
.pdf-in .cv{margin-left:auto;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;color:#767368}
|
||||
|
||||
/* direction board */
|
||||
.board{width:800px;background:#F3F3F0;padding:11px;display:flex;flex-direction:column;gap:11px}
|
||||
.board h3{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px;color:#23221E;margin:0}
|
||||
.board .why{font-size:10px;color:#3C3A34;margin:0}
|
||||
.board .cost{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.06em;text-transform:uppercase;color:#767368;margin:0}
|
||||
.board .card{background:#FFFFFF;border:1px solid #C7C6BD;padding:9px 10px;display:flex;flex-direction:column;gap:6px}
|
||||
.board .strip{border:1px solid #D7D6CE}
|
||||
"""
|
||||
|
||||
# ── icons: one 20px grid, stroke only ────────────────────────────────────────
|
||||
P = {
|
||||
"Open": ['M2 6h6l2 2h8v9H2z'],
|
||||
"Save": ['M3 3h11l3 3v11H3z', 'M6 3v5h7V3', 'M6 17v-5h8v5'],
|
||||
"Print": ['M6 8V3h8v5', 'M3 8h14v6h-3v4H6v-4H3z'],
|
||||
"Scan": ['M3 5V3h4', 'M17 5V3h-4', 'M3 15v2h4', 'M17 15v2h-4', 'M2 10h16'],
|
||||
"OCR": ['M3 6V3h3', 'M17 6V3h-3', 'M3 14v3h3', 'M17 14v3h-3', 'M6 9h8', 'M6 12h5'],
|
||||
"Select": ['M5 3l11 8-5 1 3 5-2 1-3-5-4 3z'],
|
||||
"Text": ['M4 5h12', 'M10 5v11'],
|
||||
"Mark": ['M4 15l3-9h2l3 9', 'M6 12h5', 'M14 15h3'],
|
||||
"Draw": ['M3 17l3-1 9-9-2-2-9 9z'],
|
||||
"Shape": ['M3 8h8v8H3z', 'M11 4h6v6h-6z'],
|
||||
"Sign": ['M3 14c3-6 5 3 7-2s4 2 7-3', 'M3 17h14'],
|
||||
"Stamp": ['M6 16h8', 'M7 13h6l-1-5a2 2 0 10-4 0z'],
|
||||
"Image": ['M3 4h14v12H3z', 'M3 13l4-4 3 3 3-3 4 4'],
|
||||
"Crop": ['M6 2v12h12', 'M2 6h12v12'],
|
||||
"Pages": ['M4 3h8v14H4z', 'M12 6h4v11H8'],
|
||||
"Search": ['M13 13l4 4', 'M4 9a5 5 0 1010 0 5 5 0 10-10 0'],
|
||||
"Undo": ['M7 6L3 10l4 4', 'M3 10h9a4 4 0 010 8'],
|
||||
"Redo": ['M13 6l4 4-4 4', 'M17 10H8a4 4 0 000 8'],
|
||||
"Merge": ['M3 5h6v4H3z', 'M11 11h6v4h-6z', 'M6 9v3h5'],
|
||||
"Flatten":['M6 3h8v7H6z', 'M3 13h14', 'M3 16h14'],
|
||||
}
|
||||
|
||||
|
||||
def ico(n, on=False, dim=False):
|
||||
col = "#FFFFFF" if on else ("#767368" if dim else "#23221E")
|
||||
d = "".join(f'<path d="{p}"/>' for p in P.get(n, ['M4 4h12v12H4z']))
|
||||
return (f'<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="{col}" '
|
||||
f'stroke-width="1.35" stroke-linecap="square">{d}</svg>')
|
||||
|
||||
|
||||
def T(n, on=False, dim=False):
|
||||
c = "pdf-t on" if on else ("pdf-t dim" if dim else "pdf-t")
|
||||
return f'<div class="{c}">{ico(n, on, dim)}<span class="lb">{n}</span></div>'
|
||||
|
||||
|
||||
ZOOM = ('<span class="pdf-zoom"><b>FIT WIDTH</b><i>−</i><i>+</i></span>')
|
||||
OVER = ('<div class="pdf-over"><svg width="14" height="14" viewBox="0 0 14 14" fill="#23221E">'
|
||||
'<circle cx="3" cy="7" r="1.2"/><circle cx="7" cy="7" r="1.2"/><circle cx="11" cy="7" r="1.2"/></svg></div>')
|
||||
|
||||
|
||||
def title(name="BON-2026-0847 delivery note.pdf", tag=""):
|
||||
t = f'<span class="new">{tag}</span>' if tag else ''
|
||||
return (f'<div class="w-title"><img class="mmd-logo" src="mmd-logo-red.svg" alt="MMD" '
|
||||
f'style="height:13px"><span class="app">MMD PDF</span>'
|
||||
f'<span class="name">{name}</span>{t}<span class="spring"></span>'
|
||||
f'<span class="ctl"><span>−</span><span>◻</span><span>×</span></span></div>')
|
||||
|
||||
|
||||
def strip_collapsed(n="1"):
|
||||
return (f'<div class="pdf-strip"><svg width="11" height="11" viewBox="0 0 12 12" fill="none" '
|
||||
f'stroke="#23221E" stroke-width="1.3"><path d="M4 2l4 4-4 4"/></svg>'
|
||||
f'<span class="pn">{n}</span></div>')
|
||||
|
||||
|
||||
def rail_open(pages=("1", "2", "3")):
|
||||
pg = "".join(
|
||||
f'<div class="pdf-pg{" here" if i == 0 else ""}"><div class="mini">'
|
||||
f'<i class="t"></i><i></i><i></i><i class="s"></i><i></i></div>'
|
||||
f'<div class="n">{p}</div></div>' for i, p in enumerate(pages))
|
||||
return (f'<div class="pdf-rail"><div class="pdf-rail-h"><span class="on">Pages</span>'
|
||||
f'<span>Marks</span></div>{pg}</div>')
|
||||
|
||||
|
||||
DOC = ('<h1 class="pdf-h">MMD STEEL GROUP — DELIVERY NOTE</h1>'
|
||||
'<p class="pdf-p">Supplier: Fabrimetal SARL<br>Reference: BON-2026-0847<br>'
|
||||
'Quantity delivered: 12 tonnes<br>Vehicle: DK-4471-AB · Driver: M. Sow<br><br>'
|
||||
'Received by: ______________________<br><br>'
|
||||
'Dakar plant · corridor freight · weighbridge ticket WB-4471</p>')
|
||||
|
||||
DOC_EDIT = ('<h1 class="pdf-h">MMD STEEL GROUP — DELIVERY NOTE</h1>'
|
||||
'<p class="pdf-p">Supplier: Fabrimetal SARL<br>Reference: BON-2026-0847<br>'
|
||||
'<span class="pdf-live">Quantity delivered: 19 tonnes</span><br>'
|
||||
'Vehicle: DK-4471-AB · Driver: M. Sow<br><br>'
|
||||
'Received by: ______________________<br><br>'
|
||||
'Dakar plant · corridor freight · weighbridge ticket WB-4471</p>')
|
||||
|
||||
|
||||
def status(left, right, warn=False):
|
||||
cls = "warn" if warn else ""
|
||||
return (f'<div class="pdf-status"><span class="{cls}">{left}</span>'
|
||||
f'<span class="r">{right}</span></div>')
|
||||
|
||||
|
||||
HEAD = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
||||
<style>
|
||||
%s
|
||||
%s
|
||||
</style>
|
||||
</helmet>
|
||||
""" % (RP, EXT)
|
||||
TAIL = "</x-dc>\n</body>\n</html>\n"
|
||||
|
||||
|
||||
def w(name, inner):
|
||||
open(os.path.join(OUT, name), "w", encoding="utf-8").write(HEAD + inner + TAIL)
|
||||
|
||||
|
||||
# ── Direction A: one labelled strip, fourteen tools, rest behind overflow ────
|
||||
A_TOOLS = ["Open", "Save", "Print", "Scan", "OCR"]
|
||||
A_EDIT = ["Select", "Text", "Mark", "Draw", "Shape", "Sign"]
|
||||
A_DOC = ["Pages", "Search", "Undo"]
|
||||
|
||||
|
||||
def strip_A(active="Select", h=40):
|
||||
return (f'<div class="pdf-tools" style="height:{h}px">'
|
||||
+ "".join(T(t) for t in A_TOOLS) + '<span class="pdf-rule"></span>'
|
||||
+ "".join(T(t, t == active) for t in A_EDIT) + '<span class="pdf-rule"></span>'
|
||||
+ "".join(T(t) for t in A_DOC)
|
||||
+ f'<div class="pdf-end">{ZOOM}</div>{OVER}</div>')
|
||||
|
||||
|
||||
w("Main.dc.html",
|
||||
'<div class="pdf">' + title() + strip_A("Select") +
|
||||
'<div class="pdf-work">' + strip_collapsed() +
|
||||
'<div class="pdf-canvas"><div class="pdf-page">' + DOC + '</div></div></div>' +
|
||||
status("Ready", "1 OF 1 · FIT WIDTH · 100%") + '</div>')
|
||||
|
||||
# ── Editing text, context bar present ───────────────────────────────────────
|
||||
ctx = ('<div class="pdf-ctx"><span class="lbl">Colour</span>'
|
||||
'<span class="pdf-sw"><i class="on" style="background:#23221E"></i>'
|
||||
'<i style="background:#C8102E"></i><i style="background:#3C3A34"></i>'
|
||||
'<i style="background:#767368"></i><i style="background:#FFFFFF"></i></span>'
|
||||
'<span class="lbl">Size</span><span class="pdf-pick">11 pt</span>'
|
||||
'<span class="pdf-pick">B</span><span class="pdf-pick">I</span>'
|
||||
'<span class="spring"></span>'
|
||||
'<span class="lbl">Enter keeps it · Esc throws it away</span></div>')
|
||||
w("EditText.dc.html",
|
||||
'<div class="pdf">' + title(tag="Changed") + strip_A("Text") + ctx +
|
||||
'<div class="pdf-work">' + strip_collapsed() +
|
||||
'<div class="pdf-canvas"><div class="pdf-page">' + DOC_EDIT + '</div></div></div>' +
|
||||
status("The old quantity is removed from the file when you save",
|
||||
"1 OF 1 · FIT WIDTH · 100%", warn=True) + '</div>')
|
||||
|
||||
# ── Rail open, three pages ──────────────────────────────────────────────────
|
||||
w("PageRail.dc.html",
|
||||
'<div class="pdf">' + title("Weighbridge tickets week 34.pdf") + strip_A("Select") +
|
||||
'<div class="pdf-work">' + rail_open() +
|
||||
'<div class="pdf-canvas"><div class="pdf-page">' + DOC + '</div></div></div>' +
|
||||
status("Ready", "1 OF 3 · FIT WIDTH · 100%") + '</div>')
|
||||
|
||||
# ── Nothing open ────────────────────────────────────────────────────────────
|
||||
rows = "".join(
|
||||
f'<tr><td class="ref"{" style=box-shadow:inset~3px~0~0~#C8102E".replace(chr(126),chr(32)) if i == 0 else ""}>{r}</td>'
|
||||
f'<td>{d}</td><td class="num">{n}</td><td><span class="w-state">{s}</span></td></tr>'
|
||||
for i, (r, d, n, s) in enumerate([
|
||||
("BON-2026-0847", "Delivery note — Fabrimetal SARL", "1", "Changed"),
|
||||
("INV-4471", "Supplier invoice — Fabrimetal SARL", "3", "Signed"),
|
||||
("WB-4471", "Weighbridge ticket — Dakar plant", "1", "Scanned"),
|
||||
("INSP-0912", "Mill inspection — Kigali", "8", "Read"),
|
||||
("PO-AO-2231", "Refractory bricks — Vesuvius Angola", "2", "Read"),
|
||||
("BON-2026-0839", "Delivery note — Continental Sahel", "1", "Signed"),
|
||||
("INV-4402", "Freight invoice — corridor Dakar–Bamako", "2", "Read"),
|
||||
("WB-4468", "Weighbridge ticket — Dakar plant", "1", "Scanned"),
|
||||
("PO-SN-24099", "Diesel gensets — Groupe Sénégal Énergie", "4", "Read"),
|
||||
("BON-2026-0831", "Delivery note — SMS Concast", "1", "Read"),
|
||||
("INSP-0908", "Mill inspection — Dakar", "6", "Read"),
|
||||
("WB-4455", "Weighbridge ticket — Dakar plant", "1", "Scanned"),
|
||||
("INV-4377", "Supplier invoice — RHI Magnesita", "3", "Signed"),
|
||||
("PO-RW-24107", "Rolling mill rollers — Danieli Service", "2", "Read"),
|
||||
("BON-2026-0824", "Delivery note — Nexans Angola", "1", "Read"),
|
||||
]))
|
||||
w("Nothing.dc.html",
|
||||
'<div class="pdf">' + title("No document open") +
|
||||
f'<div class="pdf-tools" style="height:40px">'
|
||||
+ "".join(T(t) for t in A_TOOLS) + '<span class="pdf-rule"></span>'
|
||||
+ "".join(T(t, dim=True) for t in A_EDIT) + '<span class="pdf-rule"></span>'
|
||||
+ "".join(T(t, dim=True) for t in A_DOC)
|
||||
+ f'<div class="pdf-end">{ZOOM}</div>{OVER}</div>'
|
||||
+ '<div class="pdf-work"><div class="pdf-canvas" style="padding:0"><div style="width:100%">'
|
||||
'<div class="w-toolbar"><h2 class="w-h1">Recent</h2>'
|
||||
'<span class="w-count">on this machine</span>'
|
||||
'<span class="w-filters"><span class="w-chip">Open a file…</span>'
|
||||
'<span class="w-cta">Scan a document</span></span></div>'
|
||||
'<table class="w-tbl"><thead><tr><th>Reference</th><th>Document</th>'
|
||||
'<th class="num">Pages</th><th>State</th></tr></thead><tbody>' + rows +
|
||||
'</tbody></table></div></div></div>' +
|
||||
status("Pick a document, or scan one", "NOTHING OPEN") + '</div>')
|
||||
|
||||
# ── Saving over the original ────────────────────────────────────────────────
|
||||
|
||||
def behind(doc=DOC, active="Select", st=("Ready", "1 OF 1 · FIT WIDTH · 100%")):
|
||||
return ('<div class="pdf-behind"><div class="pdf" style="box-shadow:none;border:0">'
|
||||
+ title() + strip_A(active)
|
||||
+ '<div class="pdf-work">' + strip_collapsed()
|
||||
+ '<div class="pdf-canvas"><div class="pdf-page">' + doc + '</div></div></div>'
|
||||
+ status(*st) + '</div></div>')
|
||||
|
||||
|
||||
w("Save.dc.html",
|
||||
'<div class="pdf-over-scrim">' + behind(DOC_EDIT, "Text",
|
||||
("The old quantity is removed from the file when you save",
|
||||
"1 OF 1 · FIT WIDTH · 100%")) +
|
||||
'<div class="pdf-dlg">'
|
||||
'<div class="pdf-dlg-t">Save</div><div class="pdf-dlg-b">'
|
||||
'<div>Write the change into <b>BON-2026-0847 delivery note.pdf</b>?</div>'
|
||||
'<div class="pdf-kv">'
|
||||
'<span class="k">Was</span><span>Quantity delivered: 12 tonnes</span>'
|
||||
'<span class="k">Becomes</span><span>Quantity delivered: 19 tonnes</span>'
|
||||
'<span class="k">The old text</span><span>is taken out of the file, not covered over</span>'
|
||||
'</div></div><div class="pdf-dlg-a">'
|
||||
'<span class="w-chip">Cancel</span><span class="w-chip">Keep both</span>'
|
||||
'<span class="w-cta">Replace</span></div></div></div>')
|
||||
|
||||
# ── Scan ────────────────────────────────────────────────────────────────────
|
||||
def field(k, v, note=""):
|
||||
n = f'<span class="cv">{note}</span>' if note else ''
|
||||
return f'<span class="k">{k}</span><span class="pdf-in">{v}{n}</span>'
|
||||
|
||||
|
||||
w("Scan.dc.html",
|
||||
'<div class="pdf-over-scrim">' + behind() + '<div class="pdf-dlg">'
|
||||
'<div class="pdf-dlg-t">Scan</div><div class="pdf-dlg-b">'
|
||||
'<div>Pages arrive in the document you have open.</div>'
|
||||
'<div class="pdf-kv">'
|
||||
+ field("Copier", "Ricoh IM C3000 — Dakar plant", "ready")
|
||||
+ field("Feed", "Both sides")
|
||||
+ field("Colour", "Greyscale")
|
||||
+ field("Detail", "300 dpi", "print")
|
||||
+ '</div>'
|
||||
'<div><span class="w-state">Text search on</span> French and English</div>'
|
||||
'</div><div class="pdf-dlg-a">'
|
||||
'<span class="w-chip">Cancel</span><span class="w-cta">Scan</span></div></div></div>')
|
||||
|
||||
# ── The three directions ────────────────────────────────────────────────────
|
||||
def strip_B(active="Select"):
|
||||
tools = A_TOOLS + A_EDIT + ["Stamp", "Image", "Crop", "Merge", "Flatten"] + A_DOC + ["Redo"]
|
||||
return ('<div class="pdf-tools pdf-tools--icons" style="height:30px">'
|
||||
+ "".join(ico_t(t, t == active) for t in tools)
|
||||
+ f'<div class="pdf-end">{ZOOM}</div></div>')
|
||||
|
||||
|
||||
def ico_t(n, on=False):
|
||||
c = "pdf-t on" if on else "pdf-t"
|
||||
return f'<div class="{c}">{ico(n, on)}</div>'
|
||||
|
||||
|
||||
def strip_C(tab="Edit"):
|
||||
tabs = ("Document", "Edit", "Pages")
|
||||
row = {"Document": A_TOOLS + ["Merge", "Flatten"],
|
||||
"Edit": A_EDIT + ["Stamp", "Image", "Crop"],
|
||||
"Pages": ["Pages", "Search", "Undo", "Redo"]}[tab]
|
||||
return ('<div class="pdf-tabs">' + "".join(
|
||||
f'<span class="pdf-tab{" on" if t == tab else ""}">{t}</span>' for t in tabs)
|
||||
+ '</div><div class="pdf-tools" style="height:34px">'
|
||||
+ "".join(T(t, t == "Select") for t in row)
|
||||
+ f'<div class="pdf-end">{ZOOM}</div></div>')
|
||||
|
||||
|
||||
board = f'''<div class="board">
|
||||
<div class="card">
|
||||
<h3>A — one row, named tools</h3>
|
||||
<div class="strip">{strip_A("Select", 40)}</div>
|
||||
<p class="why">Fourteen tools carry their name. Everything else — stamp, image, crop, merge, flatten, redo — lives behind the three dots on the right.</p>
|
||||
<p class="cost">40px of chrome · 512px left for the page · 16 of 30 tools hidden</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>B — icons only, nothing hidden</h3>
|
||||
<div class="strip">{strip_B("Select")}</div>
|
||||
<p class="why">All thirty fit, name on hover. Fastest once someone knows the icons; a plant clerk opening it for the first time has to guess.</p>
|
||||
<p class="cost">30px of chrome · 522px left for the page · nothing hidden</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>C — three tabs, named tools, nothing hidden</h3>
|
||||
<div class="strip">{strip_C("Edit")}</div>
|
||||
<p class="why">Document, Edit, Pages. Every tool keeps its name and every tool is reachable, at the price of one click to change tab and a second bar of chrome.</p>
|
||||
<p class="cost">56px of chrome · 496px left for the page · nothing hidden, one click deeper</p>
|
||||
</div>
|
||||
</div>'''
|
||||
w("Directions.dc.html", board)
|
||||
|
||||
# ── canvas ──────────────────────────────────────────────────────────────────
|
||||
GX, GY = W + 90, H + 150
|
||||
canvas = {
|
||||
"artboards": [
|
||||
{"file": "Directions.dc.html", "x": 0, "y": 0, "w": W, "h": 470,
|
||||
"title": "Three ways to fit the toolbar — pick one"},
|
||||
{"file": "Main.dc.html", "x": GX, "y": 0, "w": W, "h": H, "title": "A document open"},
|
||||
{"file": "EditText.dc.html", "x": 2 * GX, "y": 0, "w": W, "h": H, "title": "Changing text on the page"},
|
||||
{"file": "PageRail.dc.html", "x": 0, "y": GY, "w": W, "h": H, "title": "Page list open"},
|
||||
{"file": "Nothing.dc.html", "x": GX, "y": GY, "w": W, "h": H, "title": "Nothing open yet"},
|
||||
{"file": "Save.dc.html", "x": 2 * GX, "y": GY, "w": W, "h": H, "title": "Saving over the original"},
|
||||
{"file": "Scan.dc.html", "x": GX, "y": 2 * GY, "w": W, "h": H, "title": "Scanning — not built yet"},
|
||||
],
|
||||
"annotations": [
|
||||
{"id": "brief", "x": 0, "y": -150, "w": 660,
|
||||
"text": "MMD PDF — the Windows PDF editor for staff.\nEvery frame is 800x600, the plant PC screen. Styling is the new MMD shell system, taken from its own source. Official MMD lockup only.\n600 minus 28 title bar minus 20 status bar leaves 552px for toolbar and page."},
|
||||
{"id": "decision", "x": 0, "y": 490, "w": 620,
|
||||
"text": "The one thing that needs your answer: a PDF editor has about thirty tools and thirty named tools need roughly 1400px. A, B and C give up different things. Mark the one you want and the rest follows from it."},
|
||||
{"id": "scan-note", "x": GX, "y": 2 * GY - 90, "w": 420,
|
||||
"text": "Scanning does not exist in the application yet. This is the design for it, not a picture of something that works."},
|
||||
{"id": "edit-note", "x": 2 * GX, "y": -90, "w": 430,
|
||||
"text": "This is the moment the old wording is destroyed. The status bar says so, and Enter/Esc are spelled out because both were guessed wrong during testing."},
|
||||
],
|
||||
"launch": {"view": "canvas"},
|
||||
}
|
||||
json.dump(canvas, open(os.path.join(OUT, "canvas.json"), "w"), indent=2)
|
||||
print("written 7 artboards")
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"artboards": [
|
||||
{
|
||||
"file": "Directions.dc.html",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 800,
|
||||
"h": 470,
|
||||
"title": "Three ways to fit the toolbar \u2014 pick one"
|
||||
},
|
||||
{
|
||||
"file": "Main.dc.html",
|
||||
"x": 890,
|
||||
"y": 0,
|
||||
"w": 800,
|
||||
"h": 600,
|
||||
"title": "A document open"
|
||||
},
|
||||
{
|
||||
"file": "EditText.dc.html",
|
||||
"x": 1780,
|
||||
"y": 0,
|
||||
"w": 800,
|
||||
"h": 600,
|
||||
"title": "Changing text on the page"
|
||||
},
|
||||
{
|
||||
"file": "PageRail.dc.html",
|
||||
"x": 0,
|
||||
"y": 750,
|
||||
"w": 800,
|
||||
"h": 600,
|
||||
"title": "Page list open"
|
||||
},
|
||||
{
|
||||
"file": "Nothing.dc.html",
|
||||
"x": 890,
|
||||
"y": 750,
|
||||
"w": 800,
|
||||
"h": 600,
|
||||
"title": "Nothing open yet"
|
||||
},
|
||||
{
|
||||
"file": "Save.dc.html",
|
||||
"x": 1780,
|
||||
"y": 750,
|
||||
"w": 800,
|
||||
"h": 600,
|
||||
"title": "Saving over the original"
|
||||
},
|
||||
{
|
||||
"file": "Scan.dc.html",
|
||||
"x": 890,
|
||||
"y": 1500,
|
||||
"w": 800,
|
||||
"h": 600,
|
||||
"title": "Scanning \u2014 not built yet"
|
||||
}
|
||||
],
|
||||
"annotations": [
|
||||
{
|
||||
"id": "brief",
|
||||
"x": 0,
|
||||
"y": -150,
|
||||
"w": 660,
|
||||
"text": "MMD PDF \u2014 the Windows PDF editor for staff.\nEvery frame is 800x600, the plant PC screen. Styling is the new MMD shell system, taken from its own source. Official MMD lockup only.\n600 minus 28 title bar minus 20 status bar leaves 552px for toolbar and page."
|
||||
},
|
||||
{
|
||||
"id": "decision",
|
||||
"x": 0,
|
||||
"y": 490,
|
||||
"w": 620,
|
||||
"text": "The one thing that needs your answer: a PDF editor has about thirty tools and thirty named tools need roughly 1400px. A, B and C give up different things. Mark the one you want and the rest follows from it."
|
||||
},
|
||||
{
|
||||
"id": "scan-note",
|
||||
"x": 890,
|
||||
"y": 1410,
|
||||
"w": 420,
|
||||
"text": "Scanning does not exist in the application yet. This is the design for it, not a picture of something that works."
|
||||
},
|
||||
{
|
||||
"id": "edit-note",
|
||||
"x": 1780,
|
||||
"y": -90,
|
||||
"w": 430,
|
||||
"text": "This is the moment the old wording is destroyed. The status bar says so, and Enter/Esc are spelled out because both were guessed wrong during testing."
|
||||
}
|
||||
],
|
||||
"launch": {
|
||||
"view": "canvas"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,103 @@
|
||||
|
||||
body{margin:0;background:#F3F3F0;font-family:"PP Neue Machina","Neue Machina",system-ui,-apple-system,"Segoe UI",sans-serif;color:#3C3A34;-webkit-font-smoothing:antialiased}
|
||||
a{color:#C8102E} a:hover{color:#8E0B20}
|
||||
*{box-sizing:border-box}
|
||||
.root{padding:16px;background:#F3F3F0}
|
||||
.shell{width:1180px;display:flex;flex-direction:column;font-size:11.5px;background:#FFFFFF;border:1px solid #C7C6BD}
|
||||
.s-title{display:flex;align-items:center;gap:10px;padding:0 10px;height:30px;background:#C8102E;color:#fff;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:12.5px}
|
||||
.s-title .mmd-logo{height:19px;width:auto;display:block;flex:none;fill:#fff}
|
||||
.spring{flex:1}
|
||||
.s-who{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.07em;text-transform:uppercase;color:rgba(255,255,255,0.85)}
|
||||
.s-signout{font-size:10px;padding:1px 0;color:#fff;text-decoration:underline;text-underline-offset:2px}
|
||||
.s-bar{display:flex;align-items:stretch;gap:2px;padding:0 5px;background:#F7F7F3;border-bottom:1px solid #D7D6CE}
|
||||
.s-item{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11px;white-space:nowrap;border:1px solid transparent;color:#3C3A34}
|
||||
.s-item .cv{font-size:8px;opacity:0.5}
|
||||
.s-item .n{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;padding:0 4px;border:1px solid #23221E;color:#23221E;background:#FFFFFF;line-height:13px}
|
||||
.s-item.on{background:#FFFFFF;border-color:#D7D6CE;border-top:2px solid #C8102E;border-bottom-color:#FFFFFF;margin-bottom:-1px;font-weight:700;color:#23221E}
|
||||
.s-item.on .n{border-color:#23221E;color:#23221E}
|
||||
.s-tools{margin-left:auto;display:flex;align-items:center;gap:6px;padding-right:4px}
|
||||
.s-search{width:168px;padding:2px 7px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
|
||||
.s-btn{padding:2px 9px;font-size:10.5px;background:#FFFFFF;border:1px solid #C7C6BD;color:#23221E}
|
||||
.s-desk{position:relative;background:#D4D0C6;overflow:hidden}
|
||||
.s-desk.tiled{display:grid;grid-template-columns:1.35fr 1fr;grid-template-rows:auto auto;gap:10px;padding:10px;align-items:start}
|
||||
.s-desk.stacked{height:492px}
|
||||
.s-desk.oneup{padding:10px}
|
||||
.s-task{display:flex;align-items:center;gap:10px;padding:4px 8px;font-size:10.5px;background:#F1F1EC;border-top:1px solid #D7D6CE}
|
||||
.s-task .lbl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;letter-spacing:0.1em;text-transform:uppercase;color:#767368}
|
||||
.s-group{display:inline-flex;align-items:stretch;border:1px solid #D7D6CE;background:#FFFFFF}
|
||||
.s-group .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#767368;padding:2px 7px;border-right:1px solid #D7D6CE;display:flex;align-items:center;background:#F7F7F3}
|
||||
.s-group .win{padding:2px 8px;font-size:10px;color:#23221E;border-right:1px solid #D7D6CE;display:flex;align-items:center;gap:5px;background:#F1F1EC}
|
||||
.s-group .win:last-child{border-right:0}
|
||||
.s-group .win.on{box-shadow:inset 3px 0 0 #C8102E;font-weight:700;background:#FFFFFF}
|
||||
.s-group .win.min{color:#3C3A34;background:#FFFFFF;outline:1px dashed #6B6A64;outline-offset:-3px}
|
||||
|
||||
.s-task .tile{font-size:10px;padding:1px 8px;border:1px solid #C7C6BD;background:#FFFFFF;color:#3C3A34}
|
||||
/* windows — title bar made visible: darker plate, ink name, clear border; front = red top edge + shadow */
|
||||
.win-frame{background:#FFFFFF;border:1px solid #B9B8AE;display:flex;flex-direction:column;min-width:0;box-shadow:0 1px 2px rgba(35,34,30,0.12)}
|
||||
.win-frame.front{border-color:#767368;box-shadow:0 6px 16px rgba(35,34,30,0.26)}
|
||||
.stacked .win-frame{position:absolute}
|
||||
.w-title{display:flex;align-items:center;gap:8px;height:28px;padding:0 6px 0 8px;background:#E4E3DC;border-bottom:1px solid #C9C6BC;font-size:11px;color:#3C3A34}
|
||||
.front .w-title{background:#FFFFFF;color:#23221E;border-top:3px solid #C8102E;height:31px}
|
||||
.w-title .app{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.1em;text-transform:uppercase;color:#23221E;font-weight:700}
|
||||
.w-title .app::after{content:"/";margin-left:8px;color:#6B6A64}
|
||||
.w-title .name{font-family:Fraunces,Georgia,serif;font-weight:600;font-size:12px;color:#3C3A34}
|
||||
.front .w-title .name{color:#23221E;font-weight:700}
|
||||
.w-title .new{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#23221E;background:#E4E3DC;padding:0 6px;line-height:15px}
|
||||
.w-title .ctl{display:flex;gap:8px}
|
||||
.w-title .ctl span{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;color:#23221E;background:transparent;border:0}
|
||||
.w-body{padding:0;overflow:hidden}
|
||||
.w-toolbar{display:flex;align-items:center;gap:8px;padding:5px 8px;background:#FAFAF8;border-bottom:1px solid #E6E5DF}
|
||||
.w-h1{font-family:Fraunces,Georgia,serif;font-weight:700;font-size:13px;letter-spacing:-0.01em;margin:0;color:#23221E}
|
||||
.w-count{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#6B6A64}
|
||||
.w-count b{color:#C8102E;font-weight:600}
|
||||
.w-filters{margin-left:auto;display:flex;gap:4px}
|
||||
.w-chip{padding:1px 7px;font-size:9.5px;font-family:"JetBrains Mono",ui-monospace,monospace;border:1px solid #D7D6CE;background:#FFFFFF;color:#3C3A34}
|
||||
.w-chip.on{border-color:#767368;color:#23221E;font-weight:600}
|
||||
.w-cta{padding:2px 9px;font-size:10px;background:#C8102E;color:#FFFFFF;border:1px solid #C8102E;font-weight:600}
|
||||
table.w-tbl{width:100%;border-collapse:collapse;font-size:10.5px;background:#FFFFFF}
|
||||
.w-tbl th{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;text-align:left;padding:4px 8px;white-space:nowrap;background:#F1F1EC;color:#23221E;border-bottom:1px solid #D7D6CE;border-right:1px solid #E6E5DF}
|
||||
.w-tbl td{padding:4px 8px;border-bottom:1px solid #E6E5DF;border-right:1px solid #E6E5DF;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
|
||||
.w-tbl td:last-child,.w-tbl th:last-child{border-right:0}
|
||||
.w-tbl tr:nth-child(even) td{background:#FAFAF8}
|
||||
.w-tbl td.num{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;text-align:right}
|
||||
.w-tbl th.num{text-align:right}
|
||||
.w-tbl td.ref{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.03em}
|
||||
.w-tbl tr.sel td{background:#F7F7F3;box-shadow:inset 3px 0 0 #C8102E}
|
||||
.w-state{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.07em;text-transform:uppercase;padding:1px 5px;background:#F1F1EC;color:#23221E}
|
||||
.rec{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:0}
|
||||
.rec .col{padding:6px 10px 8px;border-right:1px solid #E6E5DF}
|
||||
.rec .col:last-child{border-right:0}
|
||||
.rec h4{margin:4px 0 4px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;font-weight:600;border-bottom:1px solid #E6E5DF;padding-bottom:3px}
|
||||
.kv{display:grid;grid-template-columns:96px 1fr;gap:2px 8px;font-size:10.5px;margin:0}
|
||||
.kv dt{color:#767368}
|
||||
.kv dd{margin:0;color:#23221E}
|
||||
.kv dd.mono{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10px;font-variant-numeric:tabular-nums}
|
||||
.rec-actions{display:flex;gap:6px;padding:6px 10px;border-top:1px solid #E6E5DF;background:#FAFAF8}
|
||||
.rec-actions span{padding:2px 9px;font-size:10px;border:1px solid #C7C6BD;background:#FFFFFF;color:#23221E}
|
||||
.rec-actions span.primary{color:#FFFFFF;background:#C8102E;border:1px solid #C8102E;font-weight:600}
|
||||
.steps{display:flex;gap:0;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.06em;text-transform:uppercase;border-bottom:1px solid #E6E5DF}
|
||||
.steps span{padding:3px 8px;border-right:1px solid #E6E5DF;color:#6B6A64}
|
||||
.steps span.done{color:#767368}
|
||||
.steps span.now{color:#C8102E;font-weight:700;box-shadow:inset 0 -2px 0 #C8102E}
|
||||
.rep{padding:6px 10px}
|
||||
.rep .row{display:grid;grid-template-columns:150px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid #E6E5DF;font-size:10.5px}
|
||||
.rep .row:last-child{border-bottom:0}
|
||||
.rep .name{color:#23221E;font-weight:600}
|
||||
.rep .name small{display:block;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:8.5px;letter-spacing:0.08em;text-transform:uppercase;color:#6B6A64;font-weight:400}
|
||||
.rep .hl{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;color:#3C3A34;font-variant-numeric:tabular-nums}
|
||||
/* title-bar option closeups */
|
||||
.opt{width:600px;padding:16px;background:#D4D0C6}
|
||||
.opt .cap{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:9.5px;letter-spacing:0.12em;text-transform:uppercase;color:#767368;margin:0 0 8px}
|
||||
.opt .win-frame{margin-bottom:14px}
|
||||
.opt .win-frame:last-child{margin-bottom:0}
|
||||
/* Option B: ink strip */
|
||||
.optB .front .w-title{background:#3C3A34;color:#fff;border-top:0;height:28px;border-bottom:1px solid #23221E}
|
||||
.optB .front .w-title .name{color:#fff}
|
||||
.optB .front .w-title .app{color:#fff}
|
||||
.optB .front .w-title .app::after{color:rgba(255,255,255,0.35)}
|
||||
.optB .front .w-title .ctl span{color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.6)}
|
||||
.optB .front .w-title .new{color:#fff;border-color:rgba(255,255,255,0.7)}
|
||||
/* Option C: ruled */
|
||||
.optC .w-title{background:#FFFFFF;border-bottom:2px solid #23221E;height:28px}
|
||||
.optC .front .w-title{background:#FFFFFF;border-top:3px solid #C8102E;height:31px;border-bottom:2px solid #23221E}
|
||||
.optC .win-frame{border-color:#23221E}
|
||||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||