---
title: "Projektstruktur — zenit Zig UI Doku"
description: "Legen Sie Build-Einstieg, Feature-UI, Styles und Assets dort ab, wo sie wachsen können — und wissen Sie, wem welcher Speicher gehört."
url: https://zenit.z.express/de/docs/guide/project-structure
language: de
alternate_en: https://zenit.z.express/docs/guide/project-structure.md
alternate_zh: https://zenit.z.express/zh/docs/guide/project-structure.md
alternate_es: https://zenit.z.express/es/docs/guide/project-structure.md
alternate_ja: https://zenit.z.express/ja/docs/guide/project-structure.md
alternate_ko: https://zenit.z.express/ko/docs/guide/project-structure.md
alternate_fr: https://zenit.z.express/fr/docs/guide/project-structure.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Projektstruktur

Legen Sie Build-Einstieg, Feature-UI, Styles und Assets dort ab, wo sie wachsen können — und wissen Sie, wem welcher Speicher gehört.

## Empfohlene Struktur

zenit schreibt keine Verzeichnisstruktur vor. Halten Sie die Build-Dateien stabil und legen Sie Baum, State und Styles jedes Features nebeneinander, damit Sie für eine Änderung nicht quer durchs Repo springen müssen.

`myapp/`

```
myapp/
├── build.zig
├── build.zig.zon
└── src/
    ├── main.zig
    ├── app_state.zig
    ├── assets.zig
    └── features/
        └── dashboard/
            ├── view.zig
            ├── state.zig
            └── styles.zig
```

## Aufgaben der Dateien

| Datei | Zuständig für | Vermeiden |
| --- | --- | --- |
| `main.zig` | Allocator, App-Init, Root-Mount | Seiten-UI anhäufen |
| `view.zig` | Node-Baum und Event-Verdrahtung | Rohe Farbwerte, aufwendige Datentransformationen |
| `state.zig` | Fachlicher State und seine Methoden | Langlebige Zeiger auf Nodes, die entfernt oder neu gebaut werden können |
| `styles.zig` | Token-basierte benannte Style-Funktionen, `fn (*const ui.ThemeTokens) ui.BoxStyle` | IO ausführen oder State ändern |

Style-Funktionen sind pure; Builder wie `ui.boxStyled` / `ui.vstackStyled` werten sie beim Mounten aus und spielen sie bei einem Theme-Wechsel mit den neuen Tokens erneut ab — Inline-Literal-Styles bekommen das nicht.

`styles.zig + view.zig`

```zig
// features/dashboard/styles.zig
const ui = @import("ui");

pub fn card(t: *const ui.ThemeTokens) ui.BoxStyle {
    return .{
        .padding = ui.Padding.all(t.space._6),
        .gap = t.space._4,
        .background = t.color.bg_secondary,
        .corner_radius = t.radius.xl,
    };
}

// features/dashboard/view.zig
const S = @import("styles.zig");
const panel = try ui.vstackStyled(cx, S.card, .{});
```

## Import-Grenze

Die öffentliche Oberfläche hat zwei Ebenen: Gängige Typen und Builder liegen unter `ui.X`, fortgeschrittene Funktionen sind nach Thema unter `ui.<group>.X` gruppiert (`ui.widgets`, `ui.fx`, `ui.events`, `ui.theme`, …). Alles außerhalb dieser beiden Ebenen ist intern.

```zig
const ui = @import("ui");

// Tier 1: what most UI code needs
const Node = ui.Node;
const Signal = ui.Signal;

// Tier 2: grouped by concern
const Router = ui.fx.Router;
const Button = ui.widgets.Button;
const Event = ui.events.Event;
```

Das Build-System erzwingt dies: `zenit.attach` gibt der App nur die Module `ui` und `zenit_app`, interne Module wie `render` und `gpu` lassen sich also gar nicht importieren (siehe das Diagramm in der [Übersicht](https://zenit.z.express/de/docs#architecture)).

> WARNING
> 
> **Die API ist noch vor 1.0.** zenit ist seit 0.1.0 öffentlich und nach Semver versioniert, doch vor 1.0 kann auch eine Minor-Version noch Breaking Changes bringen (jede Änderung steht in docs/MIGRATION.md). Pinnen Sie eine exakte Version oder Revision, statt einem Branch-Head zu folgen.

## Besitz und Lebensdauer

Zwei Besitzer, zwei Lebensdauern. `Cx` lebt auf Fensterebene: Es besitzt den Node-Baum und jedes von `cx.bindState` allozierte Struct bis `Cx.deinit`. `Scope` besitzt reaktive Ressourcen — Signals, Memos, Effects, `onCleanup`\-Callbacks und registrierte Ressourcen — und gibt sie bei `dispose()` frei; Kind-Scopes gehen mit ihrem Parent.

OWNERSHIP

Ein dispose des Seiten-Scopes gibt die Structs, die Sie auf dieser Seite per `bindState` angelegt haben, nicht frei — sie leben, bis das Fenster schließt. `Cx.deinit` läuft so ab: Root-Scope disposen → Nodes freigeben → State freigeben.

### Gebundener State: ein pub deinit deklarieren

Deklariert das Struct ein **pub** `deinit(*T)`, ruft das Framework es automatisch auf, bevor es das Struct bei `Cx.deinit` freigibt. Braucht das Aufräumen einen Allocator, speichern Sie ihn als Feld.

`state.zig`

```zig
const Item = struct { title: []const u8 };

const Model = struct {
    allocator: std.mem.Allocator,
    items: std.ArrayList(Item) = .empty,

    // Must be pub: Cx.deinit detects it and calls it before freeing the struct.
    pub fn deinit(self: *Model) void {
        self.items.deinit(self.allocator);
    }
};

const model = try cx.bindState(Model, .{ .allocator = cx.allocator });
// Do NOT also call scope.onCleanup(Model, model, Model.deinit):
// that would run deinit twice.
```

> WARNING
> 
> **Nicht doppelt aufräumen.** Ein pub `deinit` auf einem `bindState`\-Struct *und* ein registriertes `scope.onCleanup(…, T.deinit)` führen deinit zweimal aus — einmal beim dispose des Scopes, einmal bei `Cx.deinit`. Ein nicht-pub `deinit` wird nicht erkannt und nie automatisch aufgerufen.

### Ressourcen mit Seiten-Lebensdauer: onCleanup

Greifen Sie nur in zwei Fällen zu `scope.onCleanup`: Die Aufräumfunktion ist nicht pub, oder die Ressource muss der Lebensdauer eines Scopes (etwa einer Seite) folgen statt der des Fensters. Allozieren Sie solche Objekte selbst statt über `bindState`.

`features/dashboard/view.zig`

```zig
const PageCache = struct {
    allocator: std.mem.Allocator,
    rows: std.ArrayList(Item) = .empty,

    fn release(self: *PageCache) void {
        self.rows.deinit(self.allocator);
        self.allocator.destroy(self);
    }
};

fn mountDashboard(cx: *ui.Cx, parent: *ui.Scope) !*ui.Node {
    const scope = try parent.childScope();

    // Page-lifetime resource: freed when this scope is disposed,
    // not when the window closes.
    const cache = try cx.allocator.create(PageCache);
    cache.* = .{ .allocator = cx.allocator };
    scope.onCleanup(PageCache, cache, PageCache.release) catch |err| {
        cx.allocator.destroy(cache);
        return err;
    };

    return ui.vstack(cx, .{ .gap = cx.tokens.space._4 }, .{});
}
```

## Checkliste

-   **Halten Sie main.zig kurz.** Allocator, `App.init` und `runWith` — sonst nichts.
    
-   **Nur ui und zenit\_app importieren.** Fehlt etwas auf den beiden öffentlichen Ebenen, eröffnen Sie ein Issue, statt in Interna zu greifen.
    
-   **Fenster-Lebensdauer → bindState, Seiten-Lebensdauer → Scope.** Ersteres bekommt ein pub `deinit`, Letzteres ein `onCleanup` — nie beides.
    
-   **Styles als benannte Funktionen schreiben.** Legen Sie sie in styles.zig ab und nutzen Sie sie über die `*Styled`\-Builder, dann funktionieren Theme-Wechsel einfach.
