Compare commits
6 Commits
prototype/
...
89566627b5
| Author | SHA1 | Date | |
|---|---|---|---|
| 89566627b5 | |||
| c2247faed4 | |||
| 50ffab0252 | |||
| ea902b9d7e | |||
| a7d0ac15ee | |||
| 91fd351d59 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -9,8 +9,9 @@
|
||||
# Dioxus build output
|
||||
/dist
|
||||
|
||||
# Node (tailwind toolchain)
|
||||
node_modules/
|
||||
|
||||
# IDE
|
||||
/.idea
|
||||
|
||||
# dx erzeugt diese Datei bei jedem vollen Build neu, auch ohne Eingabedatei
|
||||
# und ohne dass sie noch verlinkt waere. Lokales Nebenprodukt, kein Quellcode.
|
||||
assets/tailwind.css
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2330,7 +2330,6 @@ dependencies = [
|
||||
"dioxus-code",
|
||||
"dioxus-code-editor",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -10,8 +10,7 @@ edition = "2021"
|
||||
dioxus = { version = "0.7.1", features = ["router"] }
|
||||
dioxus-code = "0.1.2"
|
||||
dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] }
|
||||
strum = { version = "0.28.0", features = ["strum_macros", "derive"] }
|
||||
strum_macros = "0.28.0"
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
|
||||
61
README.md
61
README.md
@@ -1,51 +1,32 @@
|
||||
# Development
|
||||
# DioNotes Client
|
||||
|
||||
Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets.
|
||||
Lokale Markdown-Notizen-App auf Basis von [Dioxus 0.7](https://dioxuslabs.com/learn/0.7), Desktop.
|
||||
|
||||
```
|
||||
project/
|
||||
├─ assets/ # Any assets that are used by the app should be placed here
|
||||
├─ src/
|
||||
│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app
|
||||
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
|
||||
```
|
||||
|
||||
### Automatic Tailwind (Dioxus 0.7+)
|
||||
|
||||
As of Dioxus 0.7, there no longer is a need to manually install tailwind. Simply `dx serve` and you're good to go!
|
||||
|
||||
Automatic tailwind is supported by checking for a file called `tailwind.css` in your app's manifest directory (next to Cargo.toml). To customize the file, use the dioxus.toml:
|
||||
|
||||
```toml
|
||||
[application]
|
||||
tailwind_input = "my.css"
|
||||
tailwind_output = "assets/out.css" # also customize the location of the out file!
|
||||
```
|
||||
|
||||
### Tailwind Manual Install
|
||||
|
||||
To use tailwind plugins or manually customize tailwind, you can can install the Tailwind CLI and use it directly.
|
||||
|
||||
### Tailwind
|
||||
1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||
2. Install the Tailwind CSS CLI: https://tailwindcss.com/docs/installation/tailwind-cli
|
||||
3. Run the following command in the root of the project to start the Tailwind CSS compiler:
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
npx @tailwindcss/cli -i ./input.css -o ./assets/tailwind.css --watch
|
||||
dx serve # Desktop-App starten (Standard-Plattform)
|
||||
dx serve --platform web # im Browser
|
||||
cargo test # Tests
|
||||
```
|
||||
|
||||
### Serving Your App
|
||||
## Aufbau
|
||||
|
||||
Run the following command in the root of your project to start developing with the default platform:
|
||||
|
||||
```bash
|
||||
dx serve
|
||||
```
|
||||
assets/css/ Design-System: tokens → base → layout → components,
|
||||
in dieser Reihenfolge in main.rs verlinkt
|
||||
src/route.rs alle Routen in einem Routable-Enum
|
||||
src/workspace.rs
|
||||
src/components/ wiederverwendbare Bausteine (CustomLink, CustomButton, …)
|
||||
src/views/ die Bildschirme: Selector und Editor
|
||||
docs/ Konventionen für Agenten, Rechercheergebnisse
|
||||
```
|
||||
|
||||
To run for a different platform, use the `--platform platform` flag. E.g.
|
||||
```bash
|
||||
dx serve --platform desktop
|
||||
```
|
||||
Es gibt **kein** Tailwind und keine npm-Toolchain — das Styling liegt vollständig in
|
||||
`assets/css/` und arbeitet mit den `--dn-*`-Tokens.
|
||||
|
||||
## Wo die Entscheidungen liegen
|
||||
|
||||
Fachbegriffe stehen in [`CONTEXT.md`](CONTEXT.md). Entscheidungen werden im
|
||||
Issue-Tracker geführt statt in `docs/adr/`; eine Wayfinder-Map indexiert pro Vorhaben,
|
||||
welche Entscheidung wo begründet ist. Siehe [`docs/agents/`](docs/agents/).
|
||||
|
||||
@@ -49,30 +49,6 @@
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
/* --- Workspace selector tab ------------------------------------------------- */
|
||||
|
||||
.workspace-selector {
|
||||
padding: var(--dn-space-2) var(--dn-space-4);
|
||||
border-radius: var(--dn-radius);
|
||||
color: var(--dn-fg-muted);
|
||||
font-size: var(--dn-text-base);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: color var(--dn-dur-fast) var(--dn-ease),
|
||||
background-color var(--dn-dur-fast) var(--dn-ease);
|
||||
}
|
||||
|
||||
.workspace-selector:hover {
|
||||
color: var(--dn-fg);
|
||||
background: var(--dn-bg-raised);
|
||||
}
|
||||
|
||||
.workspace-selector.active {
|
||||
color: var(--dn-fg-on-accent);
|
||||
background: var(--dn-accent);
|
||||
}
|
||||
|
||||
/* --- Buttons ---------------------------------------------------------------- */
|
||||
|
||||
.btn {
|
||||
|
||||
@@ -25,15 +25,6 @@
|
||||
padding: var(--dn-space-6);
|
||||
}
|
||||
|
||||
.workspace-selector-links {
|
||||
display: flex;
|
||||
gap: var(--dn-space-1);
|
||||
padding: var(--dn-space-1);
|
||||
background: var(--dn-bg-sunken);
|
||||
border: 1px solid var(--dn-border);
|
||||
border-radius: var(--dn-radius-lg);
|
||||
}
|
||||
|
||||
.workspace-selector-view {
|
||||
width: 100%;
|
||||
max-width: 34rem;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
Palette: Catppuccin Mocha, matching the editor's Theme::CATPPUCCIN_MOCHA
|
||||
so the editor pane and the app chrome are the same surface.
|
||||
|
||||
All tokens are prefixed --dn-. Tailwind v4 publishes its own --font-*,
|
||||
--text-*, --radius-* and --leading-* on :root and tree-shakes that set as
|
||||
utilities come and go, so an unprefixed token would collide silently.
|
||||
All tokens are prefixed --dn- to keep them in one namespace. The prefix was
|
||||
originally defence against Tailwind v4, which publishes its own --font-*,
|
||||
--text-* and --radius-* on :root; Tailwind is gone, the convention stays.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
/*! tailwindcss v4.1.5 | MIT License | https://tailwindcss.com */
|
||||
@layer properties;
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid;
|
||||
}
|
||||
html, :host {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
b, strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
code, kbd, samp, pre {
|
||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||
font-size: 1em;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub, sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
ol, ul, menu {
|
||||
list-style: none;
|
||||
}
|
||||
img, svg, video, canvas, audio, iframe, embed, object {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
button, input, select, optgroup, textarea, ::file-selector-button {
|
||||
font: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup {
|
||||
font-weight: bolder;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup option {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
::file-selector-button {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||
::placeholder {
|
||||
color: currentcolor;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1lh;
|
||||
text-align: inherit;
|
||||
}
|
||||
::-webkit-datetime-edit {
|
||||
display: inline-flex;
|
||||
}
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||
padding-block: 0;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
|
||||
appearance: button;
|
||||
}
|
||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[hidden]:where(:not([hidden='until-found'])) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.border {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.text-wrap {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-rotate-x: initial;
|
||||
--tw-rotate-y: initial;
|
||||
--tw-rotate-z: initial;
|
||||
--tw-skew-x: initial;
|
||||
--tw-skew-y: initial;
|
||||
--tw-border-style: solid;
|
||||
--tw-outline-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,27 @@ Create a Gitea issue.
|
||||
|
||||
Run `tea issues <index>` plus `tea comments ls <index>`.
|
||||
|
||||
## Branch- und PR-Workflow
|
||||
|
||||
Änderungen werden **nicht direkt auf `main`** committet, sondern auf einem eigenen Branch
|
||||
pro logischer Einheit — `feat/`, `fix/`, `refactor/`, `chore/`, `docs/`. Eine Einheit ist,
|
||||
was man in einem Satz begründen kann; wenn die Commit-Nachricht „und außerdem" braucht,
|
||||
sind es zwei Branches.
|
||||
|
||||
Zurück nach `main` geht es über einen **Pull Request in Gitea**, nicht per lokalem Merge:
|
||||
|
||||
```bash
|
||||
git push -u origin <branch>
|
||||
tea pulls create --title "..." --description "..."
|
||||
```
|
||||
|
||||
`--description` nimmt wie bei Issues nur eine Zeichenkette; für längere Texte eine Datei
|
||||
schreiben und mit `$(cat datei)` übergeben. Bezieht sich der PR auf ein Ticket, gehört
|
||||
`Closes #<n>` in die Beschreibung, damit Gitea es beim Mergen schließt.
|
||||
|
||||
Gemergt wird in der Weboberfläche — das Mergen ist eine Entscheidung des Menschen, nicht
|
||||
Teil der Umsetzung.
|
||||
|
||||
## Wayfinding operations
|
||||
|
||||
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "dionotes-client",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"tailwindcss": "^4.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^4.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
PROTOTYP — WEGWERFCODE. Nicht in die App übernehmen.
|
||||
|
||||
Drei Varianten des Selector-Bildschirms, umschaltbar über ?variant=A|B|C,
|
||||
dazu die Zustände über ?state=filled|empty|dialog.
|
||||
Frage, die er beantwortet: Gitea-Issue #8 "Selector-UI: Anlegen und Öffnen".
|
||||
|
||||
Doppelklicken zum Öffnen. Bindet die echte CSS des Projekts ein, damit
|
||||
Tokens und Komponenten nicht auseinanderlaufen.
|
||||
-->
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DioNotes — Selector-Prototyp</title>
|
||||
<link rel="stylesheet" href="assets/css/tokens.css">
|
||||
<link rel="stylesheet" href="assets/css/base.css">
|
||||
<link rel="stylesheet" href="assets/css/layout.css">
|
||||
<link rel="stylesheet" href="assets/css/components.css">
|
||||
<style>
|
||||
/* ---- Nur für den Prototyp. Was hier steht, ist noch kein Projekt-CSS. ---- */
|
||||
|
||||
body { margin: 0; background: var(--dn-bg-app); color: var(--dn-fg);
|
||||
font-family: var(--dn-font-ui); font-size: var(--dn-text-base); }
|
||||
|
||||
/* Listeneintrag — in allen Varianten gleich, damit die Varianten sich in der
|
||||
Struktur unterscheiden und nicht in der Zeilengestaltung. */
|
||||
.ws-item { display: flex; align-items: center; gap: var(--dn-space-3);
|
||||
width: 100%; padding: var(--dn-space-3); border: 1px solid transparent;
|
||||
border-radius: var(--dn-radius); background: transparent; color: inherit;
|
||||
text-align: left; cursor: pointer; font: inherit;
|
||||
transition: background-color var(--dn-dur-fast) var(--dn-ease); }
|
||||
.ws-item:hover { background: var(--dn-bg-raised); }
|
||||
.ws-item-body { min-width: 0; flex: 1; }
|
||||
.ws-item-name { color: var(--dn-fg); font-weight: 500; }
|
||||
.ws-item-path { color: var(--dn-fg-faint); font-family: var(--dn-font-code);
|
||||
font-size: var(--dn-text-xs); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; direction: rtl; text-align: left; }
|
||||
.ws-item.is-broken .ws-item-name { color: var(--dn-fg-muted); }
|
||||
|
||||
.ws-badge { flex: none; padding: 2px var(--dn-space-2);
|
||||
border-radius: 999px; font-size: var(--dn-text-xs); white-space: nowrap; }
|
||||
.ws-badge.unreachable { background: color-mix(in oklab, var(--dn-warning) 18%, transparent); color: var(--dn-warning); }
|
||||
.ws-badge.nomarker { background: color-mix(in oklab, var(--dn-fg-faint) 22%, transparent); color: var(--dn-fg-muted); }
|
||||
.ws-badge.damaged { background: color-mix(in oklab, var(--dn-danger) 18%, transparent); color: var(--dn-danger); }
|
||||
|
||||
.ws-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
/* Variante B */
|
||||
.vb-shell { max-width: 40rem; margin: 0 auto; padding: var(--dn-space-7) var(--dn-space-5);
|
||||
display: flex; flex-direction: column; gap: var(--dn-space-5); height: 100%;
|
||||
box-sizing: border-box; }
|
||||
.vb-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--dn-space-4); }
|
||||
.vb-title { font-size: var(--dn-text-xl); font-weight: 600; }
|
||||
.vb-list-wrap { flex: 1; min-height: 0; overflow-y: auto; }
|
||||
.vb-foot { border-top: 1px solid var(--dn-border); padding-top: var(--dn-space-4);
|
||||
display: flex; justify-content: space-between; align-items: center; gap: var(--dn-space-3); }
|
||||
|
||||
/* Variante C */
|
||||
.vc-shell { display: grid; grid-template-columns: 1fr 20rem; height: 100%; }
|
||||
.vc-main { padding: var(--dn-space-7); display: flex; flex-direction: column;
|
||||
gap: var(--dn-space-4); min-height: 0; }
|
||||
.vc-side { background: var(--dn-bg-sunken); border-left: 1px solid var(--dn-border);
|
||||
padding: var(--dn-space-6) var(--dn-space-5); display: flex; flex-direction: column;
|
||||
gap: var(--dn-space-5); }
|
||||
.vc-brand { font-size: var(--dn-text-xl); font-weight: 600; }
|
||||
.vc-tag { color: var(--dn-fg-faint); font-size: var(--dn-text-xs);
|
||||
letter-spacing: var(--dn-tracking-caps); text-transform: uppercase; }
|
||||
.vc-list-wrap { flex: 1; min-height: 0; overflow-y: auto; }
|
||||
.vc-drop { border: 1px dashed var(--dn-border-strong); border-radius: var(--dn-radius-lg);
|
||||
padding: var(--dn-space-5); text-align: center; color: var(--dn-fg-muted);
|
||||
font-size: var(--dn-text-sm); }
|
||||
@media (max-width: 720px) { .vc-shell { grid-template-columns: 1fr; }
|
||||
.vc-side { border-left: 0; border-top: 1px solid var(--dn-border); } }
|
||||
|
||||
/* Dialog — in allen Varianten identisch, er ist selbst kein Variantenthema */
|
||||
.pt-scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.55);
|
||||
display: grid; place-items: center; z-index: 50; }
|
||||
.pt-dialog { width: min(28rem, calc(100vw - 2rem)); background: var(--dn-bg-sunken);
|
||||
border: 1px solid var(--dn-border-strong); border-radius: var(--dn-radius-lg);
|
||||
box-shadow: var(--dn-shadow); padding: var(--dn-space-5);
|
||||
display: flex; flex-direction: column; gap: var(--dn-space-4); }
|
||||
.pt-dialog h2 { margin: 0; font-size: var(--dn-text-md); }
|
||||
.pt-dialog p { margin: 0; color: var(--dn-fg-muted); line-height: var(--dn-leading-normal); }
|
||||
.pt-dialog code { font-family: var(--dn-font-code); font-size: var(--dn-text-sm);
|
||||
color: var(--dn-fg); }
|
||||
.pt-actions { display: flex; justify-content: flex-end; gap: var(--dn-space-2); }
|
||||
|
||||
/* Umschalter — bewusst hässlich, damit er nicht als Teil des Entwurfs gelesen wird */
|
||||
.pt-bar { position: fixed; bottom: var(--dn-space-4); left: 50%;
|
||||
transform: translateX(-50%); z-index: 99; display: flex; align-items: center;
|
||||
gap: var(--dn-space-1); padding: var(--dn-space-1); border-radius: 999px;
|
||||
background: #000; border: 1px solid #555; box-shadow: 0 6px 24px rgb(0 0 0 / .5);
|
||||
font-family: var(--dn-font-code); font-size: var(--dn-text-xs); }
|
||||
.pt-bar button { border: 0; background: transparent; color: #fff; cursor: pointer;
|
||||
padding: 6px 10px; border-radius: 999px; font: inherit; }
|
||||
.pt-bar button:hover { background: #333; }
|
||||
.pt-bar .pt-label { padding: 0 var(--dn-space-3); color: #fff; white-space: nowrap; }
|
||||
.pt-bar .pt-sep { width: 1px; align-self: stretch; background: #444; margin: 0 4px; }
|
||||
.pt-bar button.on { background: #fff; color: #000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"><div id="stage"></div></div>
|
||||
|
||||
<div class="pt-bar">
|
||||
<button id="prev" title="Vorherige Variante (←)">◀</button>
|
||||
<span class="pt-label" id="label"></span>
|
||||
<button id="next" title="Nächste Variante (→)">▶</button>
|
||||
<span class="pt-sep"></span>
|
||||
<button data-state="filled">Liste</button>
|
||||
<button data-state="empty">Erststart</button>
|
||||
<button data-state="dialog">Rückfrage</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const WS = [
|
||||
{ name: "Notizen", path: "C:\\Users\\marvin\\Notizen" },
|
||||
{ name: "DioNotes-Testdaten", path: "D:\\Projekte\\DioNotes-Testdaten" },
|
||||
{ name: "Archiv 2024", path: "E:\\Backup\\Archiv 2024", badge: "unreachable", text: "nicht erreichbar" },
|
||||
{ name: "Ideen", path: "C:\\Users\\marvin\\Ideen", badge: "nomarker", text: "kein Workspace mehr" },
|
||||
{ name: "Rezepte", path: "C:\\Users\\marvin\\Rezepte",badge: "damaged", text: "beschädigt" },
|
||||
];
|
||||
|
||||
const esc = s => s.replace(/[&<>"]/g, c => ({ "&":"&","<":"<",">":">",'"':""" }[c]));
|
||||
const item = w => `
|
||||
<button class="ws-item ${w.badge ? "is-broken" : ""}">
|
||||
<span class="ws-item-body">
|
||||
<span class="ws-item-name">${esc(w.name)}</span>
|
||||
<span class="ws-item-path">${esc(w.path)}</span>
|
||||
</span>
|
||||
${w.badge ? `<span class="ws-badge ${w.badge}">${esc(w.text)}</span>` : ""}
|
||||
</button>`;
|
||||
const list = () => `<div class="ws-list">${WS.map(item).join("")}</div>`;
|
||||
|
||||
const dialog = `
|
||||
<div class="pt-scrim"><div class="pt-dialog">
|
||||
<h2>Ordner ist nicht leer</h2>
|
||||
<p><code>D:\\Notizen-Import</code> enthält bereits 34 Dateien. DioNotes legt darin
|
||||
nur den Ordner <code>.dionotes/</code> an und verändert nichts Vorhandenes.</p>
|
||||
<div class="pt-actions">
|
||||
<button class="btn btn-ghost">Abbrechen</button>
|
||||
<button class="btn btn-primary">Workspace anlegen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
|
||||
/* ---- Variante A — Reiter, wie der heutige Code es anlegt ---------------- */
|
||||
const A = state => `
|
||||
<div class="workspace-selector-layout">
|
||||
<div class="workspace-selector-links">
|
||||
<a class="workspace-selector" href="#">Create new Workspace</a>
|
||||
<a class="workspace-selector active" href="#">Open Workspace</a>
|
||||
</div>
|
||||
<div class="workspace-selector-view">
|
||||
${state === "empty" ? `
|
||||
<div class="empty-state">
|
||||
<span class="empty-state-title">Noch kein Workspace</span>
|
||||
<span>Lege einen an, um loszulegen.</span>
|
||||
<button class="btn btn-primary">Ordner wählen…</button>
|
||||
</div>` : list()}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/* ---- Variante B — eine Ansicht, Liste führt, Anlegen ist Nebenaktion ---- */
|
||||
const B = state => `
|
||||
<div class="vb-shell">
|
||||
<div class="vb-head">
|
||||
<span class="vb-title">Workspace öffnen</span>
|
||||
<span class="field-hint">${state === "empty" ? "" : WS.length + " bekannt"}</span>
|
||||
</div>
|
||||
<div class="vb-list-wrap">
|
||||
${state === "empty" ? `
|
||||
<div class="empty-state">
|
||||
<span class="empty-state-title">Hier ist noch nichts</span>
|
||||
<span>Der erste Workspace ist ein Ordner deiner Wahl.</span>
|
||||
</div>` : list()}
|
||||
</div>
|
||||
<div class="vb-foot">
|
||||
<span class="field-hint">Ein Workspace ist ein Ordner mit Notizen.</span>
|
||||
<button class="btn ${state === "empty" ? "btn-primary" : ""}">Neuen anlegen…</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/* ---- Variante C — Startbildschirm, zwei Spalten ------------------------- */
|
||||
const C = state => `
|
||||
<div class="vc-shell">
|
||||
<div class="vc-main">
|
||||
<span class="vc-tag">Zuletzt verwendet</span>
|
||||
<div class="vc-list-wrap">
|
||||
${state === "empty" ? `
|
||||
<div class="vc-drop">Noch keine Workspaces.<br>Rechts einen Ordner wählen.</div>`
|
||||
: list()}
|
||||
</div>
|
||||
</div>
|
||||
<aside class="vc-side">
|
||||
<div>
|
||||
<div class="vc-brand">DioNotes</div>
|
||||
<span class="field-hint">Markdown-Notizen, lokal.</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">Neuer Workspace</span>
|
||||
<button class="btn btn-primary">Ordner wählen…</button>
|
||||
<span class="field-hint">Legt <code>.dionotes/</code> im gewählten Ordner an.</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>`;
|
||||
|
||||
const V = { A: [A, "Reiter (heutiger Code)"], B: [B, "Liste führt"], C: [C, "Startbildschirm"] };
|
||||
const keys = Object.keys(V);
|
||||
const p = new URLSearchParams(location.search);
|
||||
let variant = keys.includes(p.get("variant")) ? p.get("variant") : "A";
|
||||
let state = ["filled","empty","dialog"].includes(p.get("state")) ? p.get("state") : "filled";
|
||||
|
||||
function render() {
|
||||
const [fn, name] = V[variant];
|
||||
document.getElementById("stage").innerHTML =
|
||||
fn(state === "dialog" ? "filled" : state) + (state === "dialog" ? dialog : "");
|
||||
document.getElementById("label").textContent = `${variant} — ${name}`;
|
||||
document.querySelectorAll("[data-state]").forEach(b =>
|
||||
b.classList.toggle("on", b.dataset.state === state));
|
||||
history.replaceState(null, "", `?variant=${variant}&state=${state}`);
|
||||
}
|
||||
const cycle = d => { variant = keys[(keys.indexOf(variant) + d + keys.length) % keys.length]; render(); };
|
||||
document.getElementById("prev").onclick = () => cycle(-1);
|
||||
document.getElementById("next").onclick = () => cycle(1);
|
||||
document.querySelectorAll("[data-state]").forEach(b =>
|
||||
b.onclick = () => { state = b.dataset.state; render(); });
|
||||
addEventListener("keydown", e => {
|
||||
if (/^(INPUT|TEXTAREA)$/.test(e.target.tagName) || e.target.isContentEditable) return;
|
||||
if (e.key === "ArrowLeft") cycle(-1);
|
||||
if (e.key === "ArrowRight") cycle(1);
|
||||
});
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
27
src/main.rs
27
src/main.rs
@@ -1,16 +1,18 @@
|
||||
mod views;
|
||||
mod components;
|
||||
mod route;
|
||||
mod views;
|
||||
mod workspace;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use crate::views::editor::dionotes_editor::EditorRoute;
|
||||
use crate::views::workspace_selector::selector::WorkspaceRoute;
|
||||
|
||||
use crate::route::Route;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
|
||||
// Linked individually rather than via @import in a single entry file: asset!()
|
||||
// only emits files it is given by name, so an @import chain would 404 at runtime.
|
||||
// Order matters — tokens, then baseline, then layout, then components.
|
||||
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||
const TOKENS_CSS: Asset = asset!("/assets/css/tokens.css");
|
||||
const BASE_CSS: Asset = asset!("/assets/css/base.css");
|
||||
const LAYOUT_CSS: Asset = asset!("/assets/css/layout.css");
|
||||
@@ -22,22 +24,17 @@ fn main() {
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let workspace_selected: Signal<bool> = use_signal(|| false);
|
||||
// Der offene Workspace als Context, nicht als Prop: Er wird im Selector
|
||||
// gesetzt und in der Datei-Leiste gelesen — beide hängen unter einem
|
||||
// Outlet, durch das sich keine Props durchreichen lassen.
|
||||
use_context_provider(|| Signal::new(None::<Workspace>));
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
// Tailwind first: its preflight lives in @layer base, our unlayered
|
||||
// rules win the cascade regardless, but this keeps source order honest.
|
||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||
document::Link { rel: "stylesheet", href: TOKENS_CSS }
|
||||
document::Link { rel: "stylesheet", href: BASE_CSS }
|
||||
document::Link { rel: "stylesheet", href: LAYOUT_CSS }
|
||||
document::Link { rel: "stylesheet", href: COMPONENTS_CSS }
|
||||
if workspace_selected() {
|
||||
Router::<EditorRoute> {}
|
||||
} else {
|
||||
Router::<WorkspaceRoute> {}
|
||||
}
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
36
src/route.rs
Normal file
36
src/route.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use dioxus::prelude::*;
|
||||
use strum::EnumMessage;
|
||||
|
||||
use crate::views::editor::dionotes_editor::{Editor, EditorShell, Home};
|
||||
use crate::views::workspace_selector::selector::Selector;
|
||||
|
||||
/// Alle Routen der App in einem Enum.
|
||||
///
|
||||
/// Bewusst *ein* Router statt zweier: Zwei Routable-Enums teilen sich dieselbe
|
||||
/// History, und beide hatten `/` belegt — beim Umschalten hätte der Editor eine
|
||||
/// URL zu sehen bekommen, die er nicht kennt.
|
||||
#[derive(Debug, Clone, Routable, PartialEq, EnumMessage)]
|
||||
#[rustfmt::skip]
|
||||
pub enum Route {
|
||||
#[nest("/workspace")]
|
||||
#[layout(EditorShell)]
|
||||
#[route("/")]
|
||||
#[strum(message = "Home")]
|
||||
Home {},
|
||||
#[route("/editor")]
|
||||
#[strum(message = "Editor")]
|
||||
Editor {},
|
||||
#[end_layout]
|
||||
#[end_nest]
|
||||
#[route("/")]
|
||||
Selector {},
|
||||
}
|
||||
|
||||
impl Route {
|
||||
/// Die Einträge der Datei-Leiste im Editor.
|
||||
///
|
||||
/// Explizit statt über `EnumIter`, weil das Enum jetzt auch den Selector
|
||||
/// enthält und der in dieser Leiste nichts zu suchen hat. Wermutstropfen:
|
||||
/// eine neue Editor-Route hier einzutragen erzwingt der Compiler nicht.
|
||||
pub const EDITOR_TABS: [Self; 2] = [Self::Home {}, Self::Editor {}];
|
||||
}
|
||||
@@ -1,32 +1,24 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_code::Theme;
|
||||
use dioxus_code_editor::{CodeEditor, Language};
|
||||
use strum::EnumMessage;
|
||||
|
||||
use crate::components::custom_link::custom_link_component::CustomLink;
|
||||
use strum::{EnumMessage, IntoEnumIterator};
|
||||
use strum_macros::EnumIter;
|
||||
use crate::route::Route;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
||||
|
||||
#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)]
|
||||
#[rustfmt::skip]
|
||||
pub enum EditorRoute {
|
||||
#[layout(Navbar)]
|
||||
#[route("/")]
|
||||
#[strum(message = "Home")]
|
||||
Home {},
|
||||
#[route("/editor")]
|
||||
#[strum(message = "Editor")]
|
||||
Editor {},
|
||||
}
|
||||
|
||||
/// Home page
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
pub fn Home() -> Element {
|
||||
rsx! {
|
||||
Hero {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Unveränderter Dioxus-Vorlagentext. Ersetzen, sobald klar ist, was
|
||||
// beim Öffnen eines Workspace angezeigt wird, bevor eine Notiz gewählt wurde.
|
||||
#[component]
|
||||
pub fn Hero() -> Element {
|
||||
rsx! {
|
||||
@@ -46,7 +38,7 @@ pub fn Hero() -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Editor() -> Element {
|
||||
pub fn Editor() -> Element {
|
||||
let mut source = use_signal(String::new);
|
||||
|
||||
rsx! {
|
||||
@@ -60,15 +52,34 @@ fn Editor() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared navbar component.
|
||||
/// Die Editor-Hülle: Datei-Leiste links, Editorfläche rechts.
|
||||
///
|
||||
/// Trägt zugleich den Zustands-Guard — ohne offenen Workspace hat der Editor
|
||||
/// nichts anzuzeigen. Der Fall ist real: Hot-Reload setzt den Context zurück,
|
||||
/// während die Route bestehen bleibt.
|
||||
#[component]
|
||||
fn Navbar() -> Element {
|
||||
pub fn EditorShell() -> Element {
|
||||
let workspace = use_context::<Signal<Option<Workspace>>>();
|
||||
let navigator = use_navigator();
|
||||
|
||||
use_effect(move || {
|
||||
if workspace.read().is_none() {
|
||||
navigator.replace(Route::Selector {});
|
||||
}
|
||||
});
|
||||
|
||||
if workspace.read().is_none() {
|
||||
return rsx! {};
|
||||
}
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "layout-container",
|
||||
div {
|
||||
class: "files-bar",
|
||||
for entry in EditorRoute::iter() {
|
||||
// TODO: Durch den Notiz-Baum des offenen Workspace ersetzen
|
||||
// (rekursiv eingelesen, aufklappbar). Bis dahin die Routen.
|
||||
for entry in Route::EDITOR_TABS {
|
||||
CustomLink {
|
||||
key: "{entry}",
|
||||
label: entry.get_message().unwrap_or_default(),
|
||||
@@ -79,8 +90,8 @@ fn Navbar() -> Element {
|
||||
}
|
||||
div {
|
||||
class: "editor-container",
|
||||
Outlet::<EditorRoute> { }
|
||||
Outlet::<Route> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// Create new Workspace Layout
|
||||
// TODO: Platzhalter — Formular zum Anlegen eines Workspace (Name, Pfad) und
|
||||
// Verdrahtung als Ziel von WorkspaceRoute::NewWorkspace fehlen noch.
|
||||
#[component]
|
||||
fn NewWorkspaceLayout() -> Element {
|
||||
rsx! {
|
||||
div {}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod selector;
|
||||
pub mod create_new_workspace;
|
||||
@@ -1,61 +1,26 @@
|
||||
use dioxus::prelude::*;
|
||||
use strum::{EnumMessage, IntoEnumIterator};
|
||||
use strum_macros::EnumIter;
|
||||
use crate::components::custom_link::custom_link_component::CustomLink;
|
||||
|
||||
#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)]
|
||||
#[rustfmt::skip]
|
||||
pub enum WorkspaceRoute {
|
||||
#[layout(Workspace)]
|
||||
#[route("/")]
|
||||
#[strum(message = "Create new Workspace")]
|
||||
NewWorkspace {},
|
||||
#[route("/ws_open")]
|
||||
#[strum(message = "Open Workspace")]
|
||||
OpenWorkspace {},
|
||||
}
|
||||
|
||||
/// Page to create new Workspace
|
||||
/// Der Bildschirm vor dem Editor: einen bekannten Workspace öffnen oder einen
|
||||
/// hinzufügen. Eine einzige Route, keine Reiter — die Workspace-Liste führt,
|
||||
/// das Hinzufügen ist Nebenaktion.
|
||||
#[component]
|
||||
fn NewWorkspace() -> Element {
|
||||
pub fn Selector() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
"New Workspace",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Page to open available Workspaces
|
||||
#[component]
|
||||
fn OpenWorkspace() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
"Open Workspace",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Workspace Layout
|
||||
#[component]
|
||||
fn Workspace() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "workspace-selector-layout",
|
||||
div {
|
||||
class: "workspace-selector-links",
|
||||
for route in WorkspaceRoute::iter() {
|
||||
CustomLink {
|
||||
key: "{route}",
|
||||
label: route.get_message().unwrap_or_default(),
|
||||
href: route.clone(),
|
||||
css: "workspace-selector",
|
||||
div { class: "workspace-selector-layout",
|
||||
div { class: "workspace-selector-view",
|
||||
// TODO: Workspace-Liste aus workspaces.ron laden und darstellen
|
||||
// (Ordnername, Pfad darunter, Statusabzeichen). Bis dahin nur
|
||||
// der leere Zustand.
|
||||
div { class: "empty-state",
|
||||
span { class: "empty-state-title", "Noch kein Workspace" }
|
||||
span { "Der erste Workspace ist ein Ordner deiner Wahl." }
|
||||
// TODO: Nativen Ordner-Dialog anbinden, Marker anlegen und
|
||||
// den Workspace in den Context schreiben.
|
||||
button { class: "btn btn-primary", disabled: true,
|
||||
"Workspace hinzufügen…"
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "workspace-selector-view",
|
||||
Outlet::<WorkspaceRoute> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/workspace.rs
Normal file
34
src/workspace.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Ein geöffneter Workspace — der Ordner auf der Platte, in dem die Notizen liegen.
|
||||
///
|
||||
/// Heute genau ein Feld. Der Typ existiert trotzdem als eigener Name, weil
|
||||
/// `CONTEXT.md` „Workspace" als Domänenbegriff führt und die Einstellungen aus
|
||||
/// `.dionotes/workspace.ron` hier andocken werden.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Workspace {
|
||||
pub root: PathBuf,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// Anzeigename: der Ordnername. Bewusst abgeleitet statt gespeichert —
|
||||
/// ein eigenes Namensfeld wäre nach dem ersten Umbenennen des Ordners falsch.
|
||||
pub fn name(&self) -> String {
|
||||
self.root
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| self.root.to_string_lossy().into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Workspace;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn name_is_the_folder_name() {
|
||||
let ws = Workspace { root: PathBuf::from("C:/Users/marvin/Notizen") };
|
||||
assert_eq!(ws.name(), "Notizen");
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
Reference in New Issue
Block a user