feat(shell): wire Document.components: ComponentLibrary (P2 — storage)

Mirrors the var_table wiring from commit e81e8e09. Adds the
component-library field to Document so the canonical .op loader
(and future "Save as Component" mutator) has a place to land
component definitions.

  - `Document.components: ComponentLibrary` — Default = empty.
  - Patched the same 9 test fixtures (`tests_geometry.rs` × 7 +
    `canvas_viewport.rs` + `layer_panel_tests.rs`) to seed the
    field alongside `var_table`.
  - Library lookup methods (`find_by_id` / `find_by_name` /
    `insert` with id-replace) are usable on `doc.components` from
    anywhere.

Pen-doc-adapter integration (`pen_document_to_payload` doesn't yet
carry components) + `NodeKind::Instance` variant for cross-document
instances + Components panel widget all stay pending — the data
shape lands first so the loader integration is one focused commit.

document.rs / mutators.rs both back at the 800-line cap via
doc-comment compaction (Node field docs dropped to inline naming,
`t()` Doc compressed to one line).

Tests: 187 shell-core (no new assertions; the 3 components tests
landed in commit 0fecab0a). Wasm32 build clean.
This commit is contained in:
Kayshen-X 2026-05-14 15:00:54 +08:00
parent a8561f55ce
commit 03a514f38d
5 changed files with 14 additions and 8 deletions

View file

@ -267,9 +267,8 @@ pub struct Document {
pub ui: UiState,
/// Undo / redo stacks. Push BEFORE a transactional mutation.
pub history: History,
/// Design variables + theme registry. Populated by the canonical
/// `.op` loader; lookup via `Variable::resolve` / `VariableTable::resolve`.
pub var_table: VariableTable,
pub components: ComponentLibrary,
}
/// Document undo / redo stacks. Snapshot = deep copy of the

View file

@ -26,16 +26,12 @@ impl Document {
}
}
/// Translate `key` via the active locale; falls through to `key`.
/// `key` is `&'static str` so the result is also `'static`,
/// letting callers store the string slice instead of cloning
/// it into an owned `String` per paint frame.
/// Translate `key` via active locale; falls through to `key`. `'static` to skip per-frame clones.
pub fn t(&self, key: &'static str) -> &'static str {
crate::i18n::translate(self.ui.locale, key)
}
/// Empty document with one empty default page named "Page 1".
/// Used by host smoke fixtures.
/// Empty document with one default page; used by host smoke fixtures.
pub fn empty() -> Self {
Self {
pages: vec![Page::new(1, "Page 1", Vec::new())],
@ -49,6 +45,7 @@ impl Document {
ui: UiState::default(),
history: History::default(),
var_table: VariableTable::default(),
components: ComponentLibrary::default(),
}
}
@ -97,6 +94,7 @@ impl Document {
ui: UiState::default(),
history: History::default(),
var_table: VariableTable::default(),
components: ComponentLibrary::default(),
};
debug_assert!(
doc.validate().is_ok(),

View file

@ -473,6 +473,7 @@ fn document_validate_catches_duplicate_node_id() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
let result = doc.validate();
assert!(result.is_err());
@ -508,6 +509,7 @@ fn document_validate_catches_empty_pages() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
let result = doc.validate();
assert!(result.is_err());
@ -533,6 +535,7 @@ fn document_validate_catches_empty_pages() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
let err2 = doc2.validate().unwrap_err();
assert!(
@ -563,6 +566,7 @@ fn document_selected_node_scopes_to_active_page() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
// Selection is on a non-active page → returns None.
assert!(doc.selected_node().is_none());
@ -593,6 +597,7 @@ fn document_active_page_returns_indexed_page() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
assert_eq!(doc.active_page().unwrap().name, "second");
}
@ -611,6 +616,7 @@ fn document_active_page_returns_none_when_index_out_of_range() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
assert!(doc.active_page().is_none());
}
@ -664,6 +670,7 @@ fn add_page_returns_none_on_id_overflow() {
ui: UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
let mut doc = doc;
assert_eq!(doc.add_page(), None);

View file

@ -811,6 +811,7 @@ mod tests {
ui: crate::document::UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
let viewport = CanvasViewport::from_document(&doc);
let mut backend = RecordingBackend::default();

View file

@ -136,6 +136,7 @@ fn from_document_scopes_to_active_page_only() {
ui: crate::document::UiState::default(),
history: crate::document::History::default(),
var_table: crate::document::VariableTable::default(),
components: crate::document::ComponentLibrary::default(),
};
let panel = LayerPanel::from_document(&doc);
assert_eq!(panel.items.len(), 1);