feat(ui): constrain decimal input and add stacked timed toasts with uniform actions
This commit is contained in:
@@ -27,6 +27,16 @@ class Program
|
||||
var appDataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Duempelkas");
|
||||
var isolated = false;
|
||||
#if DEBUG
|
||||
var directoryIndex = Array.IndexOf(args, "--data-directory");
|
||||
if (directoryIndex >= 0)
|
||||
{
|
||||
if (directoryIndex + 1 >= args.Length) throw new ArgumentException("Datenordner fehlt.");
|
||||
appDataDir = Path.GetFullPath(args[directoryIndex + 1]);
|
||||
isolated = true;
|
||||
}
|
||||
#endif
|
||||
Directory.CreateDirectory(appDataDir);
|
||||
|
||||
var dbPath = Path.Combine(appDataDir, "duempelkas.db");
|
||||
@@ -34,7 +44,7 @@ class Program
|
||||
// One-time compatibility: keep existing data if older versions stored DB next to the app executable.
|
||||
var legacyExeDirectory = Path.GetDirectoryName(Environment.ProcessPath ?? AppContext.BaseDirectory) ?? AppContext.BaseDirectory;
|
||||
var legacyDbPath = Path.Combine(legacyExeDirectory, "duempelkas.db");
|
||||
if (!File.Exists(dbPath) && File.Exists(legacyDbPath))
|
||||
if (!isolated && !File.Exists(dbPath) && File.Exists(legacyDbPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -50,7 +60,7 @@ class Program
|
||||
}
|
||||
}
|
||||
|
||||
appBuilder.Services.AddInfrastructure($"Data Source={dbPath}");
|
||||
appBuilder.Services.AddInfrastructure(new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = dbPath }.ToString(), appDataDir);
|
||||
|
||||
appBuilder.RootComponents.Add<Duempelkas.App.Components.App>("app");
|
||||
|
||||
@@ -72,9 +82,7 @@ class Program
|
||||
}
|
||||
|
||||
// Load saved window size or use defaults
|
||||
var settingsPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Duempelkas", "window.json");
|
||||
var settingsPath = Path.Combine(appDataDir, "window.json");
|
||||
|
||||
int savedWidth = 1280, savedHeight = 800;
|
||||
if (File.Exists(settingsPath))
|
||||
@@ -90,7 +98,7 @@ class Program
|
||||
}
|
||||
|
||||
app.MainWindow
|
||||
.SetTitle("Dümpelkas – Kassenbuch")
|
||||
.SetTitle(isolated ? "Dümpelkas – Testdaten" : "Dümpelkas – Kassenbuch")
|
||||
.SetSize(savedWidth, savedHeight)
|
||||
.SetUseOsDefaultSize(false);
|
||||
|
||||
|
||||
@@ -612,3 +612,21 @@ fieldset { min-width: 0; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.entry-recent > td { animation: none; border-bottom: 2px solid var(--color-accent); }
|
||||
}
|
||||
|
||||
/* One icon row plus two reserved text rows on every page. */
|
||||
.action-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; padding: 1rem 0; align-items: flex-start; }
|
||||
.action-toolbar .btn-nav { box-sizing: border-box; width: 10.5rem; min-width: 10.5rem; max-width: 10.5rem; height: 6.25rem; min-height: 6.25rem; padding: .45rem .6rem; font-size: .9rem !important; line-height: 1.15rem; gap: .2rem; border-radius: .5rem; border: 1px solid transparent; overflow-wrap: normal; }
|
||||
.action-toolbar .btn-nav i { display: block; font-size: 1.65rem; line-height: 1.85rem; }
|
||||
.action-toolbar .btn-nav span { display: block; min-height: 2.3rem; }
|
||||
.action-toolbar .more-actions { align-self: flex-start; position: relative; }
|
||||
.action-toolbar .more-actions[open] { padding: 0; border: 0; }
|
||||
.more-actions > button { position: absolute; z-index: 20; right: 0; min-width: 15rem; box-shadow: 0 4px 16px #0004; }
|
||||
.toast-stack { position: fixed; z-index: 3000; inset: .75rem .75rem auto auto; width: min(28rem, calc(100vw - 1.5rem)); max-height: calc(100vh - 1.5rem); overflow-y: auto; display: flex; flex-direction: column; gap: .65rem; pointer-events: none; }
|
||||
.app-toast { pointer-events: auto; padding: 1rem 2.5rem 1rem 1rem; color: var(--color-text); background: var(--color-surface); border: 1px solid var(--color-border); border-left: .35rem solid var(--color-accent); border-radius: .65rem; box-shadow: 0 5px 22px #0004; position: relative; animation: toast-enter .2s ease-out; overflow-wrap: anywhere; }
|
||||
.toast-success { border-left-color: var(--color-income); }
|
||||
.toast-error { border-left-color: var(--color-expense); }
|
||||
.app-toast p { margin: 0 0 .5rem; }
|
||||
.toast-close { position: absolute; right: .3rem; top: .3rem; background: transparent; border: 0; color: inherit; width: 2rem; height: 2rem; font-size: 1.5rem; }
|
||||
@keyframes toast-enter { from { transform: translateY(-2rem); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .app-toast { animation: none; } }
|
||||
.input-rejection:empty { display: none; }
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
<a class="dismiss">×</a>
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
<script src="_framework/blazor.webview.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Keep decimal editing predictable: reject the complete invalid edit/paste, never round it.
|
||||
(() => {
|
||||
function allowed(input, value) {
|
||||
return (input.dataset.allowNegative === 'true' ? /^-?\d*([,.]\d{0,2})?$/ : /^\d*([,.]\d{0,2})?$/).test(value);
|
||||
}
|
||||
function accept(input, text) {
|
||||
const next = input.value.slice(0, input.selectionStart) + text + input.value.slice(input.selectionEnd);
|
||||
const ok = allowed(input, next);
|
||||
const hint = document.getElementById(input.id + '-rejected');
|
||||
if (hint) hint.textContent = ok ? '' : 'Diese Eingabe wurde nicht übernommen. Bitte einen Betrag wie 12,50 eingeben.';
|
||||
return ok;
|
||||
}
|
||||
document.addEventListener('beforeinput', e => {
|
||||
if (e.target.matches('[data-amount-guard]') && e.data != null && !accept(e.target, e.data)) e.preventDefault();
|
||||
});
|
||||
document.addEventListener('paste', e => {
|
||||
if (e.target.matches('[data-amount-guard]') && !accept(e.target, e.clipboardData.getData('text').trim())) e.preventDefault();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user