zaniah 0.6.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/docs/adr/013-public-inspection-api.md +16 -0
- data/docs/adr/014-unified-command-dispatch.md +40 -0
- data/docs/adr/015-mime-clipboard-types.md +25 -0
- data/docs/components.md +18 -6
- data/docs/gallery/command-palette-dark.png +0 -0
- data/docs/gallery/command-palette-high_contrast.png +0 -0
- data/docs/gallery/command-palette-light.png +0 -0
- data/docs/gallery/components-dark.png +0 -0
- data/docs/gallery/components-high_contrast.png +0 -0
- data/docs/gallery/components-light.png +0 -0
- data/docs/inspection.md +19 -0
- data/docs/layout.md +21 -0
- data/docs/menus.md +64 -0
- data/docs/native.md +49 -0
- data/docs/text.md +19 -0
- data/docs/tui.md +3 -2
- data/examples/gallery.rb +1 -0
- data/examples/native_menu.rb +61 -0
- data/lib/zaniah/accessibility/tree.rb +9 -0
- data/lib/zaniah/app.rb +10 -1
- data/lib/zaniah/clipboard.rb +40 -0
- data/lib/zaniah/configuration.rb +1 -1
- data/lib/zaniah/devtools/inspector.rb +5 -29
- data/lib/zaniah/element.rb +14 -2
- data/lib/zaniah/input/action_registry.rb +12 -3
- data/lib/zaniah/input/dispatcher.rb +49 -3
- data/lib/zaniah/input/focus_handle.rb +3 -2
- data/lib/zaniah/input/focus_tree.rb +1 -0
- data/lib/zaniah/input/keymap.rb +39 -5
- data/lib/zaniah/inspection.rb +151 -0
- data/lib/zaniah/menu.rb +104 -0
- data/lib/zaniah/platform/headless/window.rb +155 -5
- data/lib/zaniah/platform/linux/wayland_window.rb +164 -13
- data/lib/zaniah/platform/linux/window.rb +296 -27
- data/lib/zaniah/platform/mac/app.rb +59 -4
- data/lib/zaniah/platform/mac/clipboard_data.rb +37 -0
- data/lib/zaniah/platform/mac/native_menu.rb +168 -0
- data/lib/zaniah/platform/mac/window.rb +246 -27
- data/lib/zaniah/platform/tui/window.rb +11 -0
- data/lib/zaniah/platform/windows/clipboard_data.rb +113 -0
- data/lib/zaniah/platform/windows/native_menu.rb +165 -0
- data/lib/zaniah/platform/windows/window.rb +264 -32
- data/lib/zaniah/task_executor.rb +2 -0
- data/lib/zaniah/text.rb +44 -1
- data/lib/zaniah/text_buffer.rb +2 -0
- data/lib/zaniah/ui/choice_fields.rb +12 -6
- data/lib/zaniah/ui/component.rb +7 -1
- data/lib/zaniah/ui/controls.rb +2 -0
- data/lib/zaniah/ui/data.rb +31 -1
- data/lib/zaniah/ui/editors.rb +52 -0
- data/lib/zaniah/ui/grid.rb +24 -1
- data/lib/zaniah/ui/matcher.rb +88 -0
- data/lib/zaniah/ui/modal.rb +128 -10
- data/lib/zaniah/ui/overlays.rb +141 -11
- data/lib/zaniah/ui/primitives.rb +41 -0
- data/lib/zaniah/ui/rich_text_surface.rb +3 -8
- data/lib/zaniah/ui.rb +1 -0
- data/lib/zaniah/unicode.rb +28 -0
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah/window_state.rb +36 -0
- data/lib/zaniah.rb +4 -0
- data/sig/accessibility.rbs +1 -0
- data/sig/app.rbs +4 -0
- data/sig/clipboard.rbs +26 -0
- data/sig/elements.rbs +7 -1
- data/sig/input.rbs +34 -3
- data/sig/inspection.rbs +57 -0
- data/sig/menu.rbs +31 -0
- data/sig/native.rbs +5 -5
- data/sig/platform.rbs +33 -7
- data/sig/ui.rbs +42 -4
- data/sig/window_state.rbs +11 -0
- data/sig/zaniah.rbs +8 -3
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e4185eb60f475f07fb3deee4bf07f0e7e16240771ee9199f9783d3fe13130cb
|
|
4
|
+
data.tar.gz: 05cafb090e57d8552cda0826d8db0ff1fa980107eab05ab9841824d6a18187c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5da9a69b48d186dcb04cf71cf48922d9e72fd019ff54919451f028cc211262fd8c49ecf85eb6dbbfd8b48213ad324ab98aefc9079473ace7d8a289401079ae4
|
|
7
|
+
data.tar.gz: f2b7ada32c16d932143db6cf68a9c2073e085ef0cda762aa7ed2f07ae7fdee5b9d5b1f5c3a5289b5fc54930fab9dbef1c0720e8035656c8ac700a2904012f431
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Add native macOS and Windows menus, declarative context menus, and platform-aware shortcut labels.
|
|
6
|
+
- Add MIME-keyed clipboard content on desktop backends, including HTML, PNG, file URLs, and X11 incremental transfers.
|
|
7
|
+
- Add Grid and Table copy/paste hooks with TSV and HTML data.
|
|
8
|
+
- Add window state persistence, frameless windows, and draggable title regions across native backends.
|
|
9
|
+
|
|
10
|
+
## 0.7.0 — 2026-09-24
|
|
11
|
+
|
|
12
|
+
- Add a stable frame inspection API with accessibility queries and DevTools integration.
|
|
13
|
+
- Unify focused and application commands with validation, availability, and checked state.
|
|
14
|
+
- Add headless and TUI clipboard support, including OSC 52 output.
|
|
15
|
+
- Add desktop editing shortcuts and copy, cut, paste, undo, and redo for plain and rich text while leaving conflicting TUI shortcuts unbound.
|
|
16
|
+
- Add configurable matching and accessible, highlighted results to the command palette and combobox.
|
|
17
|
+
|
|
3
18
|
## 0.6.4 — 2026-09-24
|
|
4
19
|
|
|
5
20
|
- Share Cartesian chart scales, axes, grid lines, ticks, and color-keyed legends across chart types.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ADR 013: Expose a stable inspection boundary
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
External test drivers and DevTools currently read render state through internal instance variables or maintain duplicate tree walkers. Internal layout and platform state can change without notice, which makes downstream tooling fragile. One option is to expose each underlying object directly; another is to publish a read-only view of the latest frame.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
Publish `Zaniah::Inspection.snapshot(window)` as the external read boundary, backed by small, explicit read accessors on the existing objects. DevTools uses the same boundary. Snapshot values are frozen; the `Entry#element` reference remains live only until the next frame. During the 0.x series, a breaking change to this public API requires at least one minor release of deprecation before removal.
|
|
13
|
+
|
|
14
|
+
## Consequences
|
|
15
|
+
|
|
16
|
+
Inspection is constructed only when requested, avoiding per-frame traversal cost. The small accessors and compatibility period constrain internal refactors, but test drivers no longer depend on private instance variables. Native accessibility actions still require a current node and window.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ADR 014: Route UI commands through focused handlers before app actions
|
|
2
|
+
|
|
3
|
+
- Status: Proposed
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Keyboard actions currently reach only focused handlers. Menus, palettes, context
|
|
9
|
+
menus, and accessibility actions need the same behavior, including a way to ask
|
|
10
|
+
whether an item is enabled without executing it. Routing each source separately
|
|
11
|
+
would make focus precedence and disabled states diverge.
|
|
12
|
+
|
|
13
|
+
Native menu accelerators introduce a separate risk: macOS can process a
|
|
14
|
+
`keyEquivalent` in addition to the framework keymap. Keeping both active could
|
|
15
|
+
execute one command twice.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Use `Dispatcher#perform` for all command sources. Focused handlers take precedence
|
|
20
|
+
from leaf to root, followed by `App#actions`. A focused handle can provide a
|
|
21
|
+
side-effect-free `validate` callback: `false` blocks the command, `true` enables
|
|
22
|
+
it, and `nil` defers to the next handle. `available?` asks validators and the app
|
|
23
|
+
registry without invoking action handlers. Legacy focused handlers without a
|
|
24
|
+
validator remain executable but cannot advertise availability by themselves.
|
|
25
|
+
|
|
26
|
+
An unvalidated legacy focus handler still executes before a disabled app
|
|
27
|
+
command, even though `available?` can only report the app command as disabled.
|
|
28
|
+
Such focused actions need `validate` to advertise their own availability.
|
|
29
|
+
|
|
30
|
+
Keyboard focus movement remains a dispatcher concern. Native menu shortcuts
|
|
31
|
+
must be routed through the Zaniah keymap exactly once; platform adapters must
|
|
32
|
+
verify that native `keyEquivalent` processing does not also invoke the command.
|
|
33
|
+
|
|
34
|
+
## Consequences
|
|
35
|
+
|
|
36
|
+
Menus and palettes can share dispatch and validation with keyboard input, and
|
|
37
|
+
app-wide commands remain available when no focus handler consumes them. Focused
|
|
38
|
+
actions intended for menus must provide `validate`; an opaque handler alone
|
|
39
|
+
cannot safely report support without being run. Native shortcut behavior needs
|
|
40
|
+
platform verification before this ADR can be accepted.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ADR 015: Use MIME names for clipboard representations
|
|
2
|
+
|
|
3
|
+
- Status: Proposed
|
|
4
|
+
- Date: 2026-09-24
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Applications need to place text, HTML, images, and private formats on the
|
|
9
|
+
clipboard at the same time. Each operating system names these formats
|
|
10
|
+
differently. Exposing those native names would make application copy/paste
|
|
11
|
+
logic platform-specific; using a closed enum would exclude private formats.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Use MIME strings as the public representation keys. Platform adapters map
|
|
16
|
+
common MIME names to native formats and preserve other names where the system
|
|
17
|
+
allows it. Items contain eagerly supplied, immutable data. The toolkit owns
|
|
18
|
+
transport and format negotiation, not conversion of application data.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
Applications can offer multiple formats through one API without depending on
|
|
23
|
+
an OS adapter. Adapters must maintain the native mappings and account for
|
|
24
|
+
platform-specific limits. Lazy data providers may be added later if eager
|
|
25
|
+
copies become a measured problem.
|
data/docs/components.md
CHANGED
|
@@ -31,6 +31,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
31
31
|
| L0 | `Spacer` | `(size = nil)` | fixed or flexible | none |
|
|
32
32
|
| L0 | `Card` | `(*children)`, `child` | theme surface | group |
|
|
33
33
|
| L0 | `Badge` | `(text, variant:)` | neutral/accent/success/warning/danger | text |
|
|
34
|
+
| L0 | `Kbd` | `(keys, platform:)`; `.for(action, keymap:)` | OS shortcut notation, including multi-stroke bindings | text |
|
|
34
35
|
| L0 | `Avatar` | `(name, image:, size:)` | initials or PNG | image |
|
|
35
36
|
| L0 | `Skeleton` | `(width:, height:)` | pulsing loading placeholder | progressbar/busy |
|
|
36
37
|
| L0 | `EmptyState` | `(title, message:, icon:, action:)` | compositional | group |
|
|
@@ -50,7 +51,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
50
51
|
| L2 | `Tooltip` | `(text, anchor:, side:, open:)` | top/bottom/left/right | tooltip |
|
|
51
52
|
| L2 | `Popover` | `(content, anchor:, side:, width:, height:, open:, modal:)` | flipped and viewport-clamped | group |
|
|
52
53
|
| L2 | `ContextMenu`, `Menu` | `(items, anchor:, open:)` | pointer + arrows/Home/End/Enter/Esc | menu/menuitem |
|
|
53
|
-
| L2 | `MenuBar` | `(menus)` |
|
|
54
|
+
| L2 | `MenuBar` | `(menus)` or `.from(app.menu_bar)` | declarative menu model or legacy pairs | menubar |
|
|
54
55
|
| L2 | `Dropdown` | `(label, items:, value:)`; `on_change` | menu-backed | button |
|
|
55
56
|
| L2 | `TextField` | `(value, placeholder:, label:, prefix:, suffix:, error:, max_length:, clearable:)` | IME, selection, counter | textbox |
|
|
56
57
|
| L2 | `TextArea` | TextField plus `rows:` | multiline/wrapped | textbox/multiline |
|
|
@@ -59,7 +60,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
59
60
|
| L2 | `NumberInput` | TextField plus `min:`, `max:`, `step:`; `increment`, `decrement` | numeric | textbox |
|
|
60
61
|
| L2 | `TagInput` | `(tags, separator:, ...)`; `on_tags_change` | badge list + editor | textbox |
|
|
61
62
|
| L2 | `Select` | `(items, label:, value:, disabled:)`; `on_change` | single choice | combobox |
|
|
62
|
-
| L2 | `Combobox` | `(items, value:, label:, placeholder:, disabled:)`; `on_change` | editable, filtered choices | combobox |
|
|
63
|
+
| L2 | `Combobox` | `(items, value:, label:, placeholder:, disabled:, matcher:)`; `on_change` | editable, filtered choices with highlighted matches | combobox |
|
|
63
64
|
| L2 | `MultiSelect` | `(items, value:, label:, disabled:)`; `on_change` | multiple selected badges | listbox |
|
|
64
65
|
| L2 | `DatePicker` | `(value, min:, max:, label:, disabled:)`; `on_change` | ISO date, day/week keyboard steps | combobox |
|
|
65
66
|
| L2 | `TimePicker` | `(value, step:, label:, disabled:)`; `on_change` | 24-hour time, minute/hour keyboard steps | combobox |
|
|
@@ -77,14 +78,14 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
77
78
|
| L3 | `Modal`, `Dialog` | `(content, title:, open:, close_on_scrim:, width:)` | focus trap, scrim, Esc | dialog/modal |
|
|
78
79
|
| L3 | `Drawer` | Modal plus `side:` | left/right | dialog/modal |
|
|
79
80
|
| L3 | `Toast` | `(message, variant:, queue:)`; `dismiss` | info/success/warning/danger | live status |
|
|
80
|
-
| L3 | `CommandPalette` | `(commands, open:, placeholder:)` | searchable modal | dialog/list |
|
|
81
|
+
| L3 | `CommandPalette` | `(commands, open:, placeholder:, matcher:)`; `.from(app.actions)` | searchable modal, Up/Down/Enter | dialog/list |
|
|
81
82
|
| L3 | `SplitPane` | `(first, second, orientation:, ratio:, min:, max:)`; `on_change` | horizontal/vertical, draggable separator | group/separator |
|
|
82
83
|
| L3 | `PaneGrid` | `(panes, columns:, rows:, divider_size:, minimum:, keyboard_step:)`; `replace`, `on_resize` | arbitrary resizable grid, stable pane IDs | group/separator |
|
|
83
84
|
| L3 | `Resizable` | `(content, width:, height:, min_width:, min_height:, max_width:, max_height:)`; `on_resize` | drag or keyboard resize | group/separator |
|
|
84
85
|
| L3 | `DockPanel` | `(center:, top:, right:, bottom:, left:)` | five-region layout | group |
|
|
85
86
|
| L3 | `ListView` | `(items, height:, row_height:, selected:)`; `on_select` | virtual rows and keyboard selection | list/listitem |
|
|
86
|
-
| L4 | `Table`, `DataGrid` | `(rows, columns:, height:, selection:, row_key:)`; `on_sort`, `on_select`, `on_edit` | virtual rows, sorting, resizing, editing | table/row/cell |
|
|
87
|
-
| L4 | `Grid` | `(rows:, columns:, row_height:, column_width:, frozen_rows:, frozen_columns:)`; `scroll_to`, range `selection`, `on_select`, `on_edit`, `on_fill`, `on_resize` | two-axis virtualization, frozen panes, visible-cell resize/fill
|
|
87
|
+
| L4 | `Table`, `DataGrid` | `(rows, columns:, height:, selection:, row_key:)`; `on_sort`, `on_select`, `on_edit`, `on_copy`, `on_paste` | virtual rows, sorting, resizing, editing, typed clipboard hooks | table/row/cell |
|
|
88
|
+
| L4 | `Grid` | `(rows:, columns:, row_height:, column_width:, frozen_rows:, frozen_columns:)`; `scroll_to`, range `selection`, `on_select`, `on_edit`, `on_fill`, `on_resize`, `on_copy`, `on_paste` | two-axis virtualization, frozen panes, visible-cell resize/fill and typed clipboard hooks | table |
|
|
88
89
|
| L4 | `TreeView` | `(items, height:, selected:)`; `expand`, `collapse`, `replace`, `replace_children`, `invalidate`, lazy `children` proc | arrows/Home/End | tree/treeitem |
|
|
89
90
|
| L5 | `Sparkline` | `(values, width:, height:, color:, label:)` | line + tooltip | image |
|
|
90
91
|
| L5 | `LineChart`, `BarChart`, `StackedBarChart`, `AreaChart` | `(series, width:, height:, colors:, label:)`; `AreaChart(stacked:)` | shared axes, ticks, grid lines, color-keyed legend, tooltip | image |
|
|
@@ -100,6 +101,15 @@ All input components are keyboard operable. Disabled controls remain visible but
|
|
|
100
101
|
removed from focus traversal. Overlay components close on Esc; modal overlays restore
|
|
101
102
|
the previous focus. See [TUI](tui.md) for terminal representations.
|
|
102
103
|
|
|
104
|
+
`Combobox` and `CommandPalette` default to case-insensitive substring matching in
|
|
105
|
+
input order. Pass `matcher:` to either component, or set a default with
|
|
106
|
+
`Zaniah.configure { |config| config.matcher = matcher }`. A matcher implements
|
|
107
|
+
`match(query, labels)` and returns `UI::Matcher::Match` values with an original
|
|
108
|
+
label `index`, descending `score`, and half-open UTF-8 byte `ranges` for highlighting.
|
|
109
|
+
It may also implement `refine(previous_matches, query)` for incremental queries.
|
|
110
|
+
`CommandPalette.from(app.actions)` uses registered action titles, disables
|
|
111
|
+
unavailable actions, and displays shortcuts with `UI::Kbd`. See [Menus](menus.md).
|
|
112
|
+
|
|
103
113
|
`UI::RichText` accepts UTF-8 byte ranges at grapheme boundaries. Inline styles are
|
|
104
114
|
`bold`, `italic`, `size`, `color`, `font`, and `link`; paragraphs support start,
|
|
105
115
|
center, end, and non-final-line justification, plus bullet/ordered lists and levels.
|
|
@@ -134,7 +144,9 @@ viewport.
|
|
|
134
144
|
In a table, Up/Down/Home/End/Page keys move and select rows, Shift+Up/Down extends
|
|
135
145
|
a range, and Cmd/Ctrl+A selects every row in multiple-selection mode. Sortable
|
|
136
146
|
headers and resize handles are separate Tab stops; Enter sorts and arrow/Page keys
|
|
137
|
-
resize.
|
|
147
|
+
resize. Grid and Table clipboard hooks receive half-open `Grid::Area` ranges;
|
|
148
|
+
Table areas follow the current display order without changing its stable-ID
|
|
149
|
+
selection. See [Layout](layout.md#two-axis-virtual-grid). Tree views use Up/Down to select, Right to expand or enter the first child,
|
|
138
150
|
and Left to collapse or return to the parent.
|
|
139
151
|
|
|
140
152
|
Run `bundle exec ruby tools/generate_component_gallery.rb` to rebuild the dark,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/docs/inspection.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Frame inspection
|
|
2
|
+
|
|
3
|
+
`Zaniah::Inspection` gives test drivers and DevTools a public, read-only view of the latest rendered frame. It works with headless and native windows.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
window.render(root)
|
|
7
|
+
snapshot = Zaniah::Inspection.snapshot(window)
|
|
8
|
+
save = snapshot.find(test_id: "save")
|
|
9
|
+
buttons = snapshot.where(type: Zaniah::UI::Button)
|
|
10
|
+
frontmost = snapshot.at(Zaniah::Point.new(120, 40))
|
|
11
|
+
node, path = snapshot.accessibility.query(role: :button, label: /save/i).first
|
|
12
|
+
Zaniah::Inspection.perform(window, node, :press)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`snapshot.root` and its children expose type, key, test ID, window-space bounds, resolved style, tooltip, context menu, handlers, and focus state. `snapshot.overlays` exposes tooltip, popup, and menu state; `snapshot.text_runs` contains rendered text; `snapshot.frame` contains a monotonically increasing frame number and timing statistics. `snapshot.find` returns the first match, `where` returns all matches, and `at` uses the frontmost registered hit region. A newly opened window has no root until its first render.
|
|
16
|
+
|
|
17
|
+
The snapshot containers and copied collections are frozen. `entry.element` is deliberately a live element reference: use it only in the same frame, because the next render may replace it. Accessibility nodes are copied for inspection; pass one to `Inspection.perform` to invoke an action on its current source node. Actions require a node from the current frame. To wait for a settled UI, call `Zaniah::Inspection.idle?(app)` after draining queued work and rendering dirty windows.
|
|
18
|
+
|
|
19
|
+
The public API is declared in [`sig/inspection.rbs`](../sig/inspection.rbs), and its independently runnable contract test is [`test/inspection_contract_test.rb`](../test/inspection_contract_test.rb). Breaking changes receive at least one minor version of deprecation during 0.x, as recorded in [ADR 013](adr/013-public-inspection-api.md).
|
data/docs/layout.md
CHANGED
|
@@ -75,6 +75,27 @@ apply a validated batch with a single frame request; `row_hidden?` and
|
|
|
75
75
|
destination areas; the application owns cell values and fill semantics. Shift
|
|
76
76
|
extends a range; Cmd/Ctrl-click toggles an additional cell range.
|
|
77
77
|
|
|
78
|
+
Copy and paste are opt-in: the grid never owns cell values. `on_copy` receives
|
|
79
|
+
the selected `Grid::Area` values and returns MIME-keyed content, which Zaniah
|
|
80
|
+
writes to the clipboard. `on_paste` receives the same half-open areas and all
|
|
81
|
+
available clipboard formats; the application decides how to apply them.
|
|
82
|
+
Without the corresponding hook, the action is unhandled. With a hook but no
|
|
83
|
+
selection, the action is disabled.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
grid.on_copy do |areas, _cx|
|
|
87
|
+
{"text/plain" => cells_as_tsv(areas), "text/html" => cells_as_html(areas)}
|
|
88
|
+
end
|
|
89
|
+
grid.on_paste do |areas, content, _cx|
|
|
90
|
+
paste_cells(areas, content.fetch("text/plain")) if content.types.include?("text/plain")
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`UI::Table` and `UI::DataGrid` use the same hooks. Their existing `selection`
|
|
95
|
+
remains a set of stable row IDs; hook arguments are `Grid::Area` ranges in the
|
|
96
|
+
current sorted display order, covering all columns. Disjoint selected rows
|
|
97
|
+
produce separate areas. The application retains ownership of table data.
|
|
98
|
+
|
|
78
99
|
`bench/grid.rb` measures a headless 800×600 viewport over a 1,000,000×16,000
|
|
79
100
|
grid at 23 sequential scroll positions, including cell construction, layout,
|
|
80
101
|
prepaint, and paint. `BUDGET=1 ruby bench/grid.rb` asserts a 16.67 ms median
|
data/docs/menus.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Menus and shortcuts
|
|
2
|
+
|
|
3
|
+
`Zaniah::Menu` describes commands; the application owns what each command does.
|
|
4
|
+
Register actions once and attach a menu model to the app:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
app.actions.register(:save, title: "Save") { |cx| save_document(cx) }
|
|
8
|
+
app.menu_bar = Zaniah::Menu.build do
|
|
9
|
+
app_menu
|
|
10
|
+
submenu "File" do
|
|
11
|
+
item :save
|
|
12
|
+
separator
|
|
13
|
+
submenu "Recent", items: -> { recent_items }
|
|
14
|
+
end
|
|
15
|
+
submenu "Edit" do
|
|
16
|
+
standard_edit_items
|
|
17
|
+
end
|
|
18
|
+
window_menu
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`recent_items` must return a `Zaniah::Menu` or an array of `Menu::Item` values.
|
|
23
|
+
The callback runs when that submenu opens, not when the menu bar is drawn.
|
|
24
|
+
`app_menu` and `window_menu` are macOS-only placeholders. They are omitted on
|
|
25
|
+
other platforms. A missing item title comes from `app.actions`; a missing
|
|
26
|
+
shortcut comes from the most recent keymap binding valid in an empty context.
|
|
27
|
+
Context-only bindings are not shown. Multi-stroke shortcuts appear in window
|
|
28
|
+
menus and `UI::Kbd`, but native menus show only single strokes.
|
|
29
|
+
|
|
30
|
+
For Linux, headless, and TUI, place `Zaniah::UI::MenuBar.from(app.menu_bar)` in
|
|
31
|
+
the application layout. Zaniah never inserts it automatically. Menu commands
|
|
32
|
+
use `dispatcher.perform(action, source: :menu)` and are enabled or checked by
|
|
33
|
+
`available?` and `checked?` when a menu opens. The in-window menu exposes a
|
|
34
|
+
Back row for nested submenus and restores the previously focused control so
|
|
35
|
+
standard editing commands target that control. `Inspection.snapshot(window).overlays.menu`
|
|
36
|
+
returns the app's current model. Existing `UI::MenuBar.new([[label, items]])`
|
|
37
|
+
and `UI::Menu.new([[label, callback]])` forms remain valid.
|
|
38
|
+
|
|
39
|
+
`UI::Kbd.new("cmd-shift-p", platform: :mac)` displays `⌘⇧P`; on Windows or
|
|
40
|
+
Linux it displays `Super+Shift+P`. `UI::Kbd.for(:save, keymap: keymap)` uses
|
|
41
|
+
the same binding as the menu. Its TUI text always uses readable names such as
|
|
42
|
+
`Ctrl+Shift+P`.
|
|
43
|
+
|
|
44
|
+
Windows attaches a native menu to each window. It refreshes enabled and checked
|
|
45
|
+
states when a menu opens and evaluates dynamic submenus at that time. Single-key
|
|
46
|
+
shortcuts appear after a tab in item labels; key input still runs only through
|
|
47
|
+
the Zaniah keymap, not a Win32 accelerator table. The menu model remains
|
|
48
|
+
available for inspection and in-window display on all backends; headless has
|
|
49
|
+
no system menu.
|
|
50
|
+
|
|
51
|
+
On macOS, `App#menu_bar` becomes the system main menu. The app menu uses native
|
|
52
|
+
About, Hide, Show All, and Quit items; `window_menu` uses the native Window menu.
|
|
53
|
+
Dynamic submenu callbacks run when that submenu opens. Enabled and checked
|
|
54
|
+
states are refreshed from the active window's dispatcher. Native shortcuts
|
|
55
|
+
display single-stroke keymap bindings and route key events through the keymap
|
|
56
|
+
before AppKit can run a matching menu item. A missing menu model keeps the
|
|
57
|
+
original Quit-only menu. Run `ruby examples/native_menu.rb` in a macOS graphical
|
|
58
|
+
session to inspect the native behavior.
|
|
59
|
+
|
|
60
|
+
`element.context_menu(Zaniah::Menu.build { item :save })` accepts the same
|
|
61
|
+
command model as the menu bar, including separators, nested submenus, dynamic
|
|
62
|
+
items, and current enabled/checked state. macOS and Windows use native context
|
|
63
|
+
menus for this model; headless, TUI, and `UI::ContextMenu` use the in-window
|
|
64
|
+
menu. The older `[["Save", -> { save }]]` form remains supported.
|
data/docs/native.md
CHANGED
|
@@ -42,6 +42,54 @@ file drops, fullscreen, cursors, URL opening, file dialogs, appearance changes,
|
|
|
42
42
|
and PNG capture. Platform availability differs; see [the RBS declarations](../sig/native.rbs)
|
|
43
43
|
and [platform declarations](../sig/platform.rbs) for the exact API.
|
|
44
44
|
|
|
45
|
+
## Clipboard representations
|
|
46
|
+
|
|
47
|
+
`Clipboard::Item` holds eager MIME-keyed data. `text/*` values are UTF-8 text;
|
|
48
|
+
other values are bytes. The window's `clipboard` and `clipboard=` methods remain
|
|
49
|
+
shortcuts for `text/plain`.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
item = Zaniah::Clipboard::Item.new(
|
|
53
|
+
"text/plain" => "a\tb",
|
|
54
|
+
"text/html" => "<b>a</b>",
|
|
55
|
+
"image/png" => png_bytes
|
|
56
|
+
)
|
|
57
|
+
window.write_clipboard([item])
|
|
58
|
+
window.clipboard_types # available MIME names, without reading data
|
|
59
|
+
content = window.read_clipboard(types: ["text/html", "text/plain"])
|
|
60
|
+
content.types # formats found, in requested order
|
|
61
|
+
content.fetch("text/html")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Headless keeps representations in per-window memory. TUI also keeps every
|
|
65
|
+
representation in memory and sends only `text/plain` writes to the terminal
|
|
66
|
+
with OSC 52; it cannot read the terminal's clipboard. Native macOS, Windows,
|
|
67
|
+
X11, and Wayland exchange `text/plain`, `text/html`, and `image/png` with other
|
|
68
|
+
applications. macOS also preserves arbitrary MIME types under reversible
|
|
69
|
+
`com.noxdea.zaniah.mime.<hex>` pasteboard identifiers; Windows, X11, and
|
|
70
|
+
Wayland use their native format mechanisms. Native clipboard exchange still
|
|
71
|
+
depends on a running desktop session and should be checked on each target OS.
|
|
72
|
+
|
|
73
|
+
## Window state and decorations
|
|
74
|
+
|
|
75
|
+
`window.frame` uses logical screen coordinates; `window.state.to_h` can be saved
|
|
76
|
+
by the application and passed back to `window.restore_state`. Missing displays
|
|
77
|
+
and offscreen frames are corrected toward the primary display. `maximize`,
|
|
78
|
+
`minimize`, `restore`, `fullscreen?`, `always_on_top=`, and `on_state_change`
|
|
79
|
+
operate on the native window where supported. `decorations: :native` is the
|
|
80
|
+
default; `:hidden_titlebar` and `:none` enable a custom titlebar built from
|
|
81
|
+
`window_drag_region` and `window_control` elements. `min_size:`, `resizable:`,
|
|
82
|
+
and macOS-only `traffic_lights:` are creation options.
|
|
83
|
+
|
|
84
|
+
X11 window managers must support EWMH for state and drag requests. On X11,
|
|
85
|
+
`:hidden_titlebar` has the same no-decoration effect as `:none` through Motif
|
|
86
|
+
hints. Wayland does not expose a window position: its frame origin is `nil`
|
|
87
|
+
and setting a position raises. Wayland also cannot request always-on-top or
|
|
88
|
+
programmatically restore a minimized window; those operations raise rather
|
|
89
|
+
than claim success. Client-side decorations on Wayland depend on the compositor
|
|
90
|
+
and its decoration protocol. Verify native placement and drag behavior on each
|
|
91
|
+
target desktop before relying on it.
|
|
92
|
+
|
|
45
93
|
## Displays, file watching, and terminals
|
|
46
94
|
|
|
47
95
|
`Zaniah::Platform.displays` returns the available displays for a backend.
|
|
@@ -73,6 +121,7 @@ ruby examples/native_smoke.rb --gl --check /tmp/zaniah-gl.png
|
|
|
73
121
|
ruby examples/linux_smoke.rb
|
|
74
122
|
ruby examples/linux_smoke.rb --wayland
|
|
75
123
|
ruby examples/native_watch.rb
|
|
124
|
+
ruby examples/native_menu.rb # macOS only
|
|
76
125
|
```
|
|
77
126
|
|
|
78
127
|
Linux XIM composition is exercised in CI with Xvfb and IBus/KKC. Native input,
|
data/docs/text.md
CHANGED
|
@@ -164,6 +164,25 @@ Supported inline styles are `bold`, `italic`, `size`, `color`, `font`, and
|
|
|
164
164
|
receives `(text, rich_text)`; selection changes are reported by `on_select`.
|
|
165
165
|
IME composition uses the existing `TextBuffer` composition path.
|
|
166
166
|
|
|
167
|
+
## Editing shortcuts
|
|
168
|
+
|
|
169
|
+
Editable text fields and rich text share the same actions from keyboard, menu,
|
|
170
|
+
and command dispatch. The default desktop keys are:
|
|
171
|
+
|
|
172
|
+
| Action | macOS | Windows | Linux |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| Undo / redo | Cmd+Z / Cmd+Shift+Z | Ctrl+Z / Ctrl+Y or Ctrl+Shift+Z | Ctrl+Z / Ctrl+Shift+Z |
|
|
175
|
+
| Cut / copy / paste / select all | Cmd+X/C/V/A | Ctrl+X/C/V/A | Ctrl+X/C/V/A |
|
|
176
|
+
| Previous / next word | Option+Left/Right | Ctrl+Left/Right | Ctrl+Left/Right |
|
|
177
|
+
| Document start / end | Cmd+Up/Down | Ctrl+Home/End | Ctrl+Home/End |
|
|
178
|
+
|
|
179
|
+
Shift with a word arrow extends the selection. Up/Down moves between logical
|
|
180
|
+
lines in multiline fields; Home/End moves to the current line's edges. Copy
|
|
181
|
+
requires a selection, and password fields never copy or cut. Paste and undo
|
|
182
|
+
are disabled during IME composition. The TUI does not assign these desktop
|
|
183
|
+
editing shortcuts by default because terminal control keys can conflict;
|
|
184
|
+
applications may provide a custom keymap.
|
|
185
|
+
|
|
167
186
|
## Inline and block overlays
|
|
168
187
|
|
|
169
188
|
Attach non-editable UI elements to text without inserting bytes into the source:
|
data/docs/tui.md
CHANGED
|
@@ -7,6 +7,7 @@ tests and non-window integrations.
|
|
|
7
7
|
| Component family | Terminal representation |
|
|
8
8
|
| --- | --- |
|
|
9
9
|
| Label, Badge, Avatar | text, `[badge]`, `(initials)` |
|
|
10
|
+
| Kbd | textual shortcut, such as `Ctrl+Shift+P`; multiple strokes are space-separated |
|
|
10
11
|
| Icon | Unicode check/close/search/menu/info/warning symbol; unlabeled unknown icons are omitted |
|
|
11
12
|
| Divider, Card | `─`/`│` and box-drawing characters |
|
|
12
13
|
| Skeleton, Spinner, ProgressBar, Meter | shade cells, `◌`, or a ten-cell progress track; animation jumps to its terminal state |
|
|
@@ -16,11 +17,11 @@ tests and non-window integrations.
|
|
|
16
17
|
| Text inputs | `[value]`; password values are masked and validation errors have `!` |
|
|
17
18
|
| Select, Combobox, MultiSelect | labeled brackets, filtered menu marker, or comma-separated selected values |
|
|
18
19
|
| DatePicker, TimePicker, ColorPicker | labeled ISO date, 24-hour time, or hex color in brackets |
|
|
19
|
-
| Tooltip, Popover, Menu, Dropdown | status text or a box/menu with `>` selection marker |
|
|
20
|
+
| Tooltip, Popover, Menu, MenuBar, Dropdown | status text or a box/menu with `>` selection marker; nested menus expose a Back row |
|
|
20
21
|
| Tabs, Accordion, Collapsible | selected tab in brackets and `[+]`/`[-]` disclosure markers |
|
|
21
22
|
| ScrollView, Scrollbar | clipped child content and `│`/`─` track |
|
|
22
23
|
| Breadcrumb, Pagination, bars | slash-separated path, page status, space-separated content |
|
|
23
|
-
| Modal, Dialog, Drawer, CommandPalette | box-drawing overlay; focus stays inside until dismissed |
|
|
24
|
+
| Modal, Dialog, Drawer, CommandPalette | box-drawing overlay; palette filters results and supports Up/Down/Enter; focus stays inside until dismissed |
|
|
24
25
|
| SplitPane, PaneGrid, Resizable, DockPanel | `│`/`─` separators, resize corner, or ordered dock regions |
|
|
25
26
|
| ListView | visible rows with `>` on the selected item |
|
|
26
27
|
| Toast | live status text |
|
data/examples/gallery.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Gallery
|
|
|
22
22
|
.child(Zaniah::Div.new.flex_row.items_center.gap(12).children([
|
|
23
23
|
Zaniah::UI::Label.new("Label"), Zaniah::UI::Icon.new(:info, label: "Information"),
|
|
24
24
|
Zaniah::UI::Badge.new("New", variant: :accent), Zaniah::UI::Avatar.new("Ruby UI"),
|
|
25
|
+
Zaniah::UI::Kbd.new("ctrl-shift-p", platform: :windows),
|
|
25
26
|
Zaniah::UI::Spacer.new(4), Zaniah::UI::Skeleton.new]))
|
|
26
27
|
.child(Zaniah::UI::Divider.new)
|
|
27
28
|
.child(Zaniah::UI::EmptyState.new("No results", message: "Try another query").h(64))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run on macOS with a graphical session. Check File, Edit, Window, Cmd+S,
|
|
4
|
+
# disabled and checked items, and the Recent submenu after opening it twice.
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
6
|
+
require "zaniah/ui"
|
|
7
|
+
|
|
8
|
+
raise "native_menu.rb requires macOS" unless RUBY_PLATFORM.include?("darwin")
|
|
9
|
+
|
|
10
|
+
app = Zaniah::App.new
|
|
11
|
+
count = 0
|
|
12
|
+
enabled = true
|
|
13
|
+
checked = false
|
|
14
|
+
window = nil
|
|
15
|
+
app.actions.register(:save, title: "Save", enabled: ->(_cx) { enabled }) do |_cx|
|
|
16
|
+
count += 1
|
|
17
|
+
puts "Save invoked #{count} time(s)"
|
|
18
|
+
window.request_frame
|
|
19
|
+
end
|
|
20
|
+
app.actions.register(:toggle, title: "Checked item", checked: ->(_cx) { checked }) do |_cx|
|
|
21
|
+
checked = !checked
|
|
22
|
+
window.request_frame
|
|
23
|
+
end
|
|
24
|
+
app.actions.register(:enable_save, title: "Enable Save") do |_cx|
|
|
25
|
+
enabled = !enabled
|
|
26
|
+
puts "Save enabled: #{enabled}"
|
|
27
|
+
end
|
|
28
|
+
app.menu_bar = Zaniah::Menu.build do
|
|
29
|
+
app_menu
|
|
30
|
+
submenu "File" do
|
|
31
|
+
item :save
|
|
32
|
+
item :enable_save
|
|
33
|
+
separator
|
|
34
|
+
submenu "Recent", items: -> { Zaniah::Menu.build { item :save, title: "Example.txt" }.items }
|
|
35
|
+
end
|
|
36
|
+
submenu "Edit" do
|
|
37
|
+
standard_edit_items
|
|
38
|
+
separator
|
|
39
|
+
item :toggle
|
|
40
|
+
end
|
|
41
|
+
window_menu
|
|
42
|
+
end
|
|
43
|
+
window = app.open_window(backend: :mac, width: 500, height: 180, title: "Zaniah native menu") do
|
|
44
|
+
Zaniah::Div.new.p(24).child(Zaniah::UI::Label.new("Open File/Edit/Window; press Cmd+S. Saves: #{count}"))
|
|
45
|
+
end
|
|
46
|
+
window.dispatcher.keymap.bind("cmd-s", :save)
|
|
47
|
+
if ARGV.include?("--check")
|
|
48
|
+
window.tick
|
|
49
|
+
cocoa = Zaniah::Platform::Mac::O
|
|
50
|
+
menu = cocoa.send(Zaniah::Platform::Mac::App.instance.handle, "mainMenu")
|
|
51
|
+
count = cocoa.send(menu, "numberOfItems", result: :long)
|
|
52
|
+
raise "native main menu was not installed (#{count} items)" unless count == 4
|
|
53
|
+
app.menu_bar = nil
|
|
54
|
+
window.tick
|
|
55
|
+
fallback = cocoa.send(Zaniah::Platform::Mac::App.instance.handle, "mainMenu")
|
|
56
|
+
raise "Quit-only menu was not restored" unless cocoa.send(fallback, "numberOfItems", result: :long) == 1
|
|
57
|
+
puts "Native menu installed"
|
|
58
|
+
window.close
|
|
59
|
+
else
|
|
60
|
+
app.run
|
|
61
|
+
end
|
|
@@ -36,6 +36,15 @@ module Zaniah
|
|
|
36
36
|
|
|
37
37
|
def find(&predicate) = each.find { |node, _path| predicate.call(node) }&.first
|
|
38
38
|
|
|
39
|
+
def query(role: nil, label: nil, states: {})
|
|
40
|
+
raise ArgumentError, "label must be a String or Regexp" unless label.nil? || label.is_a?(String) || label.is_a?(Regexp)
|
|
41
|
+
each.select do |node, _path|
|
|
42
|
+
(role.nil? || node.role == role) &&
|
|
43
|
+
(label.nil? || (node.label && (label.is_a?(Regexp) ? label.match?(node.label) : node.label == label))) &&
|
|
44
|
+
states.all? { |key, value| node.states.key?(key) && node.states[key] == value }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
def perform(node, action)
|
|
40
49
|
owner = @action_owners&.[](node.object_id)
|
|
41
50
|
owner&.accessibility_action(node, action)
|
data/lib/zaniah/app.rb
CHANGED
|
@@ -7,13 +7,14 @@ require_relative "task_executor"
|
|
|
7
7
|
|
|
8
8
|
module Zaniah
|
|
9
9
|
class App
|
|
10
|
-
attr_reader :windows, :executor
|
|
10
|
+
attr_reader :windows, :executor, :actions, :menu_bar
|
|
11
11
|
|
|
12
12
|
def initialize(clock: MONOTONIC_CLOCK)
|
|
13
13
|
@slots, @generations, @free, @windows, @globals = [], [], [], [], {theme: Theme.dark}
|
|
14
14
|
@listeners, @effects, @updating, @flushing = {}, [], 0, false
|
|
15
15
|
@clock = clock
|
|
16
16
|
@executor = TaskExecutor.new(clock: clock)
|
|
17
|
+
@actions = Input::ActionRegistry.new
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def new_entity
|
|
@@ -107,6 +108,7 @@ module Zaniah
|
|
|
107
108
|
options[:clock] ||= @clock
|
|
108
109
|
window = Platform.open_window(**options)
|
|
109
110
|
window.app = self
|
|
111
|
+
window.dispatcher.window = window
|
|
110
112
|
@windows << window
|
|
111
113
|
@globals[:theme] = platform_theme(window)
|
|
112
114
|
window.on_appearance do |appearance|
|
|
@@ -129,6 +131,13 @@ module Zaniah
|
|
|
129
131
|
@executor.shutdown
|
|
130
132
|
end
|
|
131
133
|
|
|
134
|
+
def menu_bar=(menu)
|
|
135
|
+
raise ArgumentError, "menu_bar must be a Zaniah::Menu or nil" unless menu.nil? || menu.is_a?(Menu)
|
|
136
|
+
@menu_bar = menu
|
|
137
|
+
@windows.each(&:request_frame)
|
|
138
|
+
menu
|
|
139
|
+
end
|
|
140
|
+
|
|
132
141
|
def hot_reload(paths, **options, &block)
|
|
133
142
|
require_relative "devtools"
|
|
134
143
|
reload = DevTools::HotReload.new(self, paths, **options, &block)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Clipboard
|
|
5
|
+
class Item
|
|
6
|
+
attr_reader :types, :formats
|
|
7
|
+
|
|
8
|
+
def initialize(formats)
|
|
9
|
+
raise TypeError, "clipboard formats must be a Hash" unless formats.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
@formats = formats.each_with_object({}) do |(type, value), result|
|
|
12
|
+
unless type.is_a?(String) && type.match?(/\A[^\s\/]+\/[^\s\/]+\z/)
|
|
13
|
+
raise ArgumentError, "invalid clipboard MIME type #{type.inspect}"
|
|
14
|
+
end
|
|
15
|
+
raise TypeError, "clipboard data must be a String" unless value.is_a?(String)
|
|
16
|
+
|
|
17
|
+
data = value.dup
|
|
18
|
+
if type.start_with?("text/")
|
|
19
|
+
data = data.encoding == Encoding::BINARY ? data.force_encoding(Encoding::UTF_8) : data.encode(Encoding::UTF_8)
|
|
20
|
+
raise Encoding::InvalidByteSequenceError, "invalid UTF-8 clipboard text" unless data.valid_encoding?
|
|
21
|
+
else
|
|
22
|
+
data.force_encoding(Encoding::BINARY)
|
|
23
|
+
end
|
|
24
|
+
result[type.dup.freeze] = data.freeze
|
|
25
|
+
end.freeze
|
|
26
|
+
@types = @formats.keys.freeze
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fetch(type) = @formats.fetch(type)
|
|
31
|
+
|
|
32
|
+
def ==(other) = other.instance_of?(self.class) && @formats == other.formats
|
|
33
|
+
alias eql? ==
|
|
34
|
+
def hash = [self.class, @formats].hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Content < Item
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/zaniah/configuration.rb
CHANGED