zaniah 0.10.0 → 0.12.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +9 -28
  4. data/assets/fonts/NotoSans-LICENSE.txt +94 -0
  5. data/assets/fonts/NotoSans-Regular.ttf +0 -0
  6. data/docs/_templates/page.erb +123 -0
  7. data/docs/adr/021-dock-layout-tree.md +33 -0
  8. data/docs/components.md +43 -5
  9. data/docs/gallery/command-palette-dark.png +0 -0
  10. data/docs/gallery/command-palette-light.png +0 -0
  11. data/docs/gallery/components-dark.png +0 -0
  12. data/docs/gallery/components-high_contrast.png +0 -0
  13. data/docs/gallery/components-light.png +0 -0
  14. data/docs/gallery/dialog-dark.png +0 -0
  15. data/docs/gallery/dialog-light.png +0 -0
  16. data/docs/gallery/hover-card-dark.png +0 -0
  17. data/docs/gallery/hover-card-high_contrast.png +0 -0
  18. data/docs/gallery/hover-card-light.png +0 -0
  19. data/docs/gallery/modal-dark.png +0 -0
  20. data/docs/gallery/modal-light.png +0 -0
  21. data/docs/gallery/popover-bottom-dark.png +0 -0
  22. data/docs/gallery/popover-bottom-light.png +0 -0
  23. data/docs/gallery/popover-left-dark.png +0 -0
  24. data/docs/gallery/popover-left-high_contrast.png +0 -0
  25. data/docs/gallery/popover-left-light.png +0 -0
  26. data/docs/gallery/popover-right-dark.png +0 -0
  27. data/docs/gallery/popover-right-light.png +0 -0
  28. data/docs/gallery/popover-top-dark.png +0 -0
  29. data/docs/gallery/popover-top-light.png +0 -0
  30. data/docs/gallery/toast-danger-dark.png +0 -0
  31. data/docs/gallery/toast-danger-light.png +0 -0
  32. data/docs/gallery/toast-info-dark.png +0 -0
  33. data/docs/gallery/toast-info-light.png +0 -0
  34. data/docs/gallery/toast-success-dark.png +0 -0
  35. data/docs/gallery/toast-success-light.png +0 -0
  36. data/docs/gallery/toast-warning-dark.png +0 -0
  37. data/docs/gallery/toast-warning-light.png +0 -0
  38. data/docs/gallery/tooltip-bottom-dark.png +0 -0
  39. data/docs/gallery/tooltip-bottom-light.png +0 -0
  40. data/docs/gallery/tooltip-left-dark.png +0 -0
  41. data/docs/gallery/tooltip-left-light.png +0 -0
  42. data/docs/gallery/tooltip-right-dark.png +0 -0
  43. data/docs/gallery/tooltip-right-light.png +0 -0
  44. data/docs/gallery/tooltip-top-dark.png +0 -0
  45. data/docs/gallery/tooltip-top-light.png +0 -0
  46. data/docs/inspection.md +3 -1
  47. data/docs/layout.md +0 -8
  48. data/docs/native.md +47 -17
  49. data/docs/pages.yml +949 -0
  50. data/docs/previews/actions.png +0 -0
  51. data/docs/previews/choices.png +0 -0
  52. data/docs/previews/command-palette-dark.png +0 -0
  53. data/docs/previews/data.png +0 -0
  54. data/docs/previews/dialog-dark.png +0 -0
  55. data/docs/previews/drawer-right-dark.png +0 -0
  56. data/docs/previews/editors.png +0 -0
  57. data/docs/previews/feedback.png +0 -0
  58. data/docs/previews/forms.png +0 -0
  59. data/docs/previews/foundation.png +0 -0
  60. data/docs/previews/grid.png +0 -0
  61. data/docs/previews/hex-view.png +0 -0
  62. data/docs/previews/menu-dark.png +0 -0
  63. data/docs/previews/navigation.png +0 -0
  64. data/docs/previews/pane-grid.png +0 -0
  65. data/docs/previews/popover-right-dark.png +0 -0
  66. data/docs/previews/structure.png +0 -0
  67. data/docs/previews/text-input.png +0 -0
  68. data/docs/previews/toast-success-dark.png +0 -0
  69. data/docs/previews/tooltip-bottom-dark.png +0 -0
  70. data/docs/previews/values.png +0 -0
  71. data/docs/previews/variants.png +0 -0
  72. data/docs/previews/workspace.png +0 -0
  73. data/docs/process_pool.md +1 -6
  74. data/docs/site.css +89 -29
  75. data/docs/text.md +0 -10
  76. data/docs/tui.md +10 -2
  77. data/docs/vector_and_list.md +15 -7
  78. data/examples/gallery.rb +35 -9
  79. data/lib/zaniah/accessibility/linux/service.rb +1 -1
  80. data/lib/zaniah/accessibility/mac.rb +1 -0
  81. data/lib/zaniah/accessibility/node.rb +2 -0
  82. data/lib/zaniah/accessibility/tree.rb +3 -2
  83. data/lib/zaniah/accessibility/windows/provider.rb +1 -0
  84. data/lib/zaniah/clipboard.rb +23 -0
  85. data/lib/zaniah/element.rb +47 -2
  86. data/lib/zaniah/gpu/software.rb +26 -8
  87. data/lib/zaniah/input/data_drop.rb +25 -0
  88. data/lib/zaniah/input/dispatcher.rb +1 -0
  89. data/lib/zaniah/input/keymap.rb +37 -0
  90. data/lib/zaniah/input/magnify.rb +7 -0
  91. data/lib/zaniah/input.rb +2 -0
  92. data/lib/zaniah/inspection.rb +9 -2
  93. data/lib/zaniah/layout/engine.rb +1 -1
  94. data/lib/zaniah/list/height_index.rb +14 -0
  95. data/lib/zaniah/list.rb +24 -3
  96. data/lib/zaniah/platform/file_dialog_options.rb +26 -0
  97. data/lib/zaniah/platform/headless/window.rb +72 -4
  98. data/lib/zaniah/platform/linux/wayland_window.rb +81 -11
  99. data/lib/zaniah/platform/linux/window.rb +167 -16
  100. data/lib/zaniah/platform/mac/window.rb +129 -12
  101. data/lib/zaniah/platform/windows/drag_drop.rb +273 -0
  102. data/lib/zaniah/platform/windows/window.rb +44 -7
  103. data/lib/zaniah/scene.rb +2 -2
  104. data/lib/zaniah/ui/calendar.rb +215 -0
  105. data/lib/zaniah/ui/component.rb +3 -1
  106. data/lib/zaniah/ui/dock_layout.rb +102 -0
  107. data/lib/zaniah/ui/dock_workspace.rb +271 -0
  108. data/lib/zaniah/ui/feedback.rb +122 -0
  109. data/lib/zaniah/ui/hex_view.rb +185 -0
  110. data/lib/zaniah/ui/hover_card.rb +95 -0
  111. data/lib/zaniah/ui/layout_components.rb +9 -1
  112. data/lib/zaniah/ui/property_grid.rb +154 -0
  113. data/lib/zaniah/ui/text_inputs.rb +130 -9
  114. data/lib/zaniah/ui/tree_view.rb +37 -7
  115. data/lib/zaniah/ui/virtual_table.rb +227 -0
  116. data/lib/zaniah/ui/zoom_pan_view.rb +106 -0
  117. data/lib/zaniah/ui.rb +9 -0
  118. data/lib/zaniah/uniform_list.rb +33 -3
  119. data/lib/zaniah/version.rb +1 -1
  120. data/sig/clipboard.rbs +20 -0
  121. data/sig/elements.rbs +15 -3
  122. data/sig/gpu.rbs +1 -1
  123. data/sig/input.rbs +28 -4
  124. data/sig/native.rbs +4 -4
  125. data/sig/ui.rbs +140 -0
  126. data/tools/build_docs.rb +101 -0
  127. data/tools/generate_component_gallery.rb +19 -10
  128. data/tools/generate_doc_previews.rb +69 -0
  129. metadata +48 -3
  130. data/docs/index.html +0 -74
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bad32e7fbe4f1abe64589fbc41d73f242c4ed3aeee9f760c67a4d205d0c8d8d7
4
- data.tar.gz: b57324bacd30eeea83d04407f689e9ca47d79178d1f0228acf5396fa2ea27881
3
+ metadata.gz: 8bda306c4301dbab842c0014a31bd563366fd846755c1adab9b864704dcd04a2
4
+ data.tar.gz: 94a9f0005a5e03e97f64f362e9ace3c1ec1757a2f73b9bf893f4e4eee6c56926
5
5
  SHA512:
6
- metadata.gz: 5bbf0bebdad77e095cb6faba2a614cf5129558dd10629c94f2e6ad9966f9cb50dfe32a37b551569cdcfb1ba74085d4c64a3667740ff1699fa7b8ce03c553ad4c
7
- data.tar.gz: 9d2491ddb19ce68065497f484b9bba0aa4e026fa998a31e3bca3d7bd41f0ed6edd805f0096aefd28ce2b1acf9309f0b96785cbbce88d67ebea10526094060a2f
6
+ metadata.gz: 0ed9ec96438a5e8de2c420b13d709be7865ad5a1df0d5e0d41cb23a110a2fe00dfc9143624a4fa4ead856e02f9b63c9e986df2f89ecee7e1f113e6265bfa6485
7
+ data.tar.gz: 118e347c3b1fe1ddc83e5f5a3390fd4dfb026ca95b79d32239c328960b1dc3a91525e7e70f1b60ef24570026f4f997dd0bb82f955a12c3f67653bd8db90d4032
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 — 2026-10-02
4
+
5
+ - Add streaming virtual tables with lazy cells, stable selection, row styling, configurable columns, and tail following.
6
+ - Add virtual hexadecimal and ASCII byte views with linked selection, highlights, keyboard navigation, and copying.
7
+ - Preserve scrolling and height measurements when virtual list counts change, with optional bottom following.
8
+ - Avoid stale rows and duplicated drawing when scrolling keyed virtual lists.
9
+ - Reduce layout work for fixed-size containers and reuse unchanged visible byte rows for faster redraws.
10
+ - Speed up vector recording by reusing resolved paint colors.
11
+ - Add text-field validation states and token completion with keyboard selection and Unicode-safe replacement.
12
+ - Add programmatic tree selection and item context menus.
13
+ - Add suggested filenames, starting directories, and extension filters for native file dialogs.
14
+ - Fix inspection snapshots for nested UI components, including select controls in dialogs.
15
+ - Preserve accessible content and actions inside wrapped split panes.
16
+
17
+ ## 0.11.0 — 2026-09-25
18
+
19
+ - Add segmented controls, alerts, hover cards, calendar and date-range inputs.
20
+ - Add zoomable views, dockable workspaces, and editable property grids.
21
+ - Add typed drag-and-drop across desktop backends with explicit move completion.
22
+
3
23
  ## 0.10.0 — 2026-09-24
4
24
 
5
25
  - Add Unicode bidirectional text layout, caret affinity, and Arabic joining.
data/README.md CHANGED
@@ -114,7 +114,7 @@ For an interactive window, choose `backend: :mac`, `:linux`, `:windows`, or
114
114
  | `:tui` | ANSI terminal | Text-grid rendering and terminal input |
115
115
 
116
116
  Native backends use the operating system libraries through Ruby's Fiddle. See
117
- [Native backends](docs/native.md) for platform requirements and checks.
117
+ [Native backends](https://noxdea.github.io/zaniah/docs/guides/native.html) for platform requirements.
118
118
 
119
119
  Native macOS, Windows, and X11 windows can be moved to a display returned by
120
120
  `Zaniah::Platform.displays` with `window.move_to_display(display)`. Headless and
@@ -124,37 +124,18 @@ compositor to fullscreen on that output, but the compositor may choose otherwise
124
124
 
125
125
  ## Documentation
126
126
 
127
- - [Native backends](docs/native.md) — windows, displays, file watching, and terminals
128
- - [Text system](docs/text.md) — font discovery, shaping, rasterization, and caching
129
- - [Images](docs/images.md) — PNG, GIF, baseline JPEG, and orientation support
130
- - [SVG and lists](docs/vector_and_list.md) — static vector icons and virtual lists
131
- - [Drag-and-drop reordering](docs/drag_drop.md) — stable IDs, virtual targets, and keyboard access
132
- - [Layout and scrolling](docs/layout.md) — Grid, resizable pane grids, scrolling, sticky positioning, and RTL foundations
133
- - [Themes](docs/theme.md) — semantic tokens and state styles
134
- - [Components](docs/components.md) — controls, overlays, data views, charts, and forms
135
- - [Animation](docs/animation.md) — easing, springs, transitions, and reduced motion
136
- - [Accessibility](docs/accessibility.md) — semantic trees, diffs, and native bridges
137
- - [Terminal UI](docs/tui.md) — deterministic component degradation
138
- - [CPU process workers](docs/process_pool.md) — portable background CPU work
139
- - [Declarative elements](docs/describe.md) — validated vocabularies, event routing, diffs, and surfaces
140
- - [Architecture decisions](docs/adr) — design records and rationale
127
+ - [Getting started](https://noxdea.github.io/zaniah/docs/) — installation and your first render
128
+ - [Guides](https://noxdea.github.io/zaniah/docs/guides/) — platforms, layout, text, themes, accessibility, and advanced APIs
129
+ - [Components](https://noxdea.github.io/zaniah/docs/components/) — rendered examples, code, and API summaries
130
+ - [Component API reference](https://noxdea.github.io/zaniah/docs/guides/component-reference.html) — all constructors and variants in one table
131
+ - [Contributing guidelines](.github/CONTRIBUTING.md) — development and platform checks
132
+ - [Architecture decisions](docs/adr) — implementation rationale
141
133
  - [RBS declarations](sig) — public API signatures
142
134
 
143
- ## Development
144
-
145
- ```sh
146
- bundle install
147
- bundle exec rake test
148
- bundle exec rake bench
149
- gem build --strict zaniah.gemspec
150
- ```
151
-
152
- Set `BUDGET=1` to enable benchmark assertions.
153
-
154
135
  ## Contributing
155
136
 
156
- Bug reports and pull requests are welcome at
157
- [github.com/noxdea/zaniah](https://github.com/noxdea/zaniah).
137
+ Bug reports and pull requests are welcome. See the
138
+ [contributing guidelines](.github/CONTRIBUTING.md) for checks and documentation updates.
158
139
 
159
140
  ## License
160
141
 
@@ -0,0 +1,94 @@
1
+ Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)
2
+
3
+ This Font Software is licensed under the SIL Open Font License,
4
+ Version 1.1.
5
+
6
+ This license is copied below, and is also available with a FAQ at:
7
+ http://scripts.sil.org/OFL
8
+
9
+ -----------------------------------------------------------
10
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11
+ -----------------------------------------------------------
12
+
13
+ PREAMBLE
14
+ The goals of the Open Font License (OFL) are to stimulate worldwide
15
+ development of collaborative font projects, to support the font
16
+ creation efforts of academic and linguistic communities, and to
17
+ provide a free and open framework in which fonts may be shared and
18
+ improved in partnership with others.
19
+
20
+ The OFL allows the licensed fonts to be used, studied, modified and
21
+ redistributed freely as long as they are not sold by themselves. The
22
+ fonts, including any derivative works, can be bundled, embedded,
23
+ redistributed and/or sold with any software provided that any reserved
24
+ names are not used by derivative works. The fonts and derivatives,
25
+ however, cannot be released under any other type of license. The
26
+ requirement for fonts to remain under this license does not apply to
27
+ any document created using the fonts or their derivatives.
28
+
29
+ DEFINITIONS
30
+ "Font Software" refers to the set of files released by the Copyright
31
+ Holder(s) under this license and clearly marked as such. This may
32
+ include source files, build scripts and documentation.
33
+
34
+ "Reserved Font Name" refers to any names specified as such after the
35
+ copyright statement(s).
36
+
37
+ "Original Version" refers to the collection of Font Software
38
+ components as distributed by the Copyright Holder(s).
39
+
40
+ "Modified Version" refers to any derivative made by adding to,
41
+ deleting, or substituting -- in part or in whole -- any of the
42
+ components of the Original Version, by changing formats or by porting
43
+ the Font Software to a new environment.
44
+
45
+ "Author" refers to any designer, engineer, programmer, technical
46
+ writer or other person who contributed to the Font Software.
47
+
48
+ PERMISSION & CONDITIONS
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of the Font Software, to use, study, copy, merge, embed,
51
+ modify, redistribute, and sell modified and unmodified copies of the
52
+ Font Software, subject to the following conditions:
53
+
54
+ 1) Neither the Font Software nor any of its individual components, in
55
+ Original or Modified Versions, may be sold by itself.
56
+
57
+ 2) Original or Modified Versions of the Font Software may be bundled,
58
+ redistributed and/or sold with any software, provided that each copy
59
+ contains the above copyright notice and this license. These can be
60
+ included either as stand-alone text files, human-readable headers or
61
+ in the appropriate machine-readable metadata fields within text or
62
+ binary files as long as those fields can be easily viewed by the user.
63
+
64
+ 3) No Modified Version of the Font Software may use the Reserved Font
65
+ Name(s) unless explicit written permission is granted by the
66
+ corresponding Copyright Holder. This restriction only applies to the
67
+ primary font name as presented to the users.
68
+
69
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70
+ Software shall not be used to promote, endorse or advertise any
71
+ Modified Version, except to acknowledge the contribution(s) of the
72
+ Copyright Holder(s) and the Author(s) or with their explicit written
73
+ permission.
74
+
75
+ 5) The Font Software, modified or unmodified, in part or in whole,
76
+ must be distributed entirely under this license, and must not be
77
+ distributed under any other license. The requirement for fonts to
78
+ remain under this license does not apply to any document created using
79
+ the Font Software.
80
+
81
+ TERMINATION
82
+ This license becomes null and void if any of the above conditions are
83
+ not met.
84
+
85
+ DISCLAIMER
86
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94
+ OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file
@@ -0,0 +1,123 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="description" content="<%= h(page.fetch("lead")) %>">
7
+ <meta name="theme-color" content="#0b1220">
8
+ <link rel="canonical" href="https://noxdea.github.io/zaniah/docs/<%= path == "index.html" ? "" : path %>">
9
+ <link rel="stylesheet" href="<%= link_from(path, "site.css") %>">
10
+ <title><%= h(page.fetch("title")) %> · Zaniah Docs</title>
11
+ </head>
12
+ <body>
13
+ <a class="skip-link" href="#content">Skip to content</a>
14
+ <header class="site-header">
15
+ <div class="header-inner">
16
+ <a class="brand" href="<%= link_from(path, "index.html") %>" aria-label="Zaniah documentation home"><span class="brand-symbol" aria-hidden="true">Z</span><span>Zaniah <small>Docs</small></span></a>
17
+ <nav class="header-links" aria-label="Global navigation">
18
+ <a href="<%= link_from(path, "index.html") %>">Docs</a>
19
+ <a href="<%= link_from(path, "guides/index.html") %>">Guides</a>
20
+ <a href="<%= link_from(path, "components/index.html") %>">Components</a>
21
+ <a href="https://github.com/noxdea/zaniah">GitHub</a>
22
+ </nav>
23
+ </div>
24
+ </header>
25
+
26
+ <div class="docs-shell">
27
+ <aside class="sidebar" aria-label="Documentation navigation">
28
+ <% groups.each do |group, items| %>
29
+ <div class="nav-group"><h2><%= h(group) %></h2>
30
+ <% items.each do |item| %>
31
+ <a href="<%= link_from(path, item.fetch("path")) %>"<%= ' aria-current="page"' if item["path"] == path %>><%= h(item.fetch("title")) %></a>
32
+ <% end %>
33
+ </div>
34
+ <% end %>
35
+ </aside>
36
+
37
+ <main id="content" class="content">
38
+ <details class="mobile-nav"><summary>Browse documentation</summary><nav aria-label="Mobile documentation navigation">
39
+ <% groups.each do |group, items| %>
40
+ <strong><%= h(group) %></strong>
41
+ <% items.each do |item| %>
42
+ <a href="<%= link_from(path, item.fetch("path")) %>"<%= ' aria-current="page"' if item["path"] == path %>><%= h(item.fetch("title")) %></a>
43
+ <% end %>
44
+ <% end %>
45
+ </nav></details>
46
+ <div class="page-heading">
47
+ <p class="breadcrumb"><a href="<%= link_from(path, "index.html") %>">Docs</a><span aria-hidden="true">/</span><%= h(page.fetch("group")) %></p>
48
+ <h1><%= h(page.fetch("title")) %></h1>
49
+ <p class="lead"><%= inline(page.fetch("lead")) %></p>
50
+ </div>
51
+ <% if page["kind"] == "component" %>
52
+ <section class="doc-section" id="import"><h2>Import</h2>
53
+ <p>Load the optional component layer before using the examples below.</p>
54
+ <div class="code-example"><div class="code-label">Ruby</div><pre><code>require "zaniah/ui"</code></pre></div>
55
+ </section>
56
+ <% end %>
57
+
58
+ <% if page["kind"] == "index" %>
59
+ <% index_groups.each do |group| %>
60
+ <% items = groups.fetch(group) %>
61
+ <% items = items.reject { |item| item["kind"] == "index" } %>
62
+ <section class="doc-section" id="<%= h(group.downcase.gsub(/[^a-z0-9]+/, "-")) %>">
63
+ <h2><%= h(group) %></h2>
64
+ <div class="directory">
65
+ <% items.each do |item| %>
66
+ <a href="<%= link_from(path, item.fetch("path")) %>"><strong><%= h(item.fetch("title")) %></strong><span><%= h(item.fetch("lead")) %></span></a>
67
+ <% end %>
68
+ </div>
69
+ </section>
70
+ <% end %>
71
+ <% elsif page["kind"] == "markdown" %>
72
+ <article class="markdown"><%= guide_html %></article>
73
+ <% else %>
74
+ <% page.fetch("sections", []).each do |section| %>
75
+ <section class="doc-section" id="<%= section_id(section) %>">
76
+ <h2><%= h(section.fetch("heading")) %></h2>
77
+ <% Array(section["text"]).each do |paragraph| %><p><%= inline(paragraph) %></p><% end %>
78
+ <% if section["image"] %>
79
+ <% image_width, image_height = image_dimensions(section.fetch("image")) %>
80
+ <figure class="example"><div class="example-label">Rendered example</div><img src="<%= link_from(path, section.fetch("image")) %>" alt="<%= h(section.fetch("alt")) %>" width="<%= image_width %>" height="<%= image_height %>" loading="lazy"><figcaption>Rendered by Zaniah's headless backend.</figcaption></figure>
81
+ <% end %>
82
+ <% if section["code"] %>
83
+ <div class="code-example"><div class="code-label"><%= h(section.fetch("code_label", "Ruby")) %></div><pre><code><%= h(section.fetch("code")) %></code></pre></div>
84
+ <% end %>
85
+ <% if section["note"] %><p class="note"><%= inline(section.fetch("note")) %></p><% end %>
86
+ </section>
87
+ <% end %>
88
+ <% if page["kind"] == "component" %>
89
+ <section class="doc-section" id="api"><h2>API summary</h2><p>Constructor options and behaviors are drawn from the <a href="<%= link_from(path, "guides/component-reference.html") %>">component reference</a>.</p>
90
+ <div class="table-scroll"><table><thead><tr><th>Component</th><th>Constructor and methods</th><th>Variants or behavior</th></tr></thead><tbody>
91
+ <% rows.each do |row| %><tr><td><code><%= h(row[:names].join(", ")) %></code></td><td><%= inline(row[:constructor]) %></td><td><%= inline(row[:variants]) %></td></tr><% end %>
92
+ </tbody></table></div>
93
+ </section>
94
+ <section class="doc-section" id="accessibility"><h2>Accessibility</h2>
95
+ <% if page["a11y"] %><p><%= inline(page["a11y"]) %></p><% end %>
96
+ <div class="table-scroll"><table><thead><tr><th>Component</th><th>Semantic role</th></tr></thead><tbody>
97
+ <% rows.each do |row| %><tr><td><code><%= h(row[:names].join(", ")) %></code></td><td><%= h(row[:role]) %></td></tr><% end %>
98
+ </tbody></table></div>
99
+ </section>
100
+ <% end %>
101
+ <% end %>
102
+
103
+ <nav class="pagination" aria-label="Adjacent pages">
104
+ <% if previous_page %><a href="<%= link_from(path, previous_page.fetch("path")) %>"><small>Previous</small><strong><%= h(previous_page.fetch("title")) %></strong></a><% end %>
105
+ <% if next_page %><a href="<%= link_from(path, next_page.fetch("path")) %>"><small>Next</small><strong><%= h(next_page.fetch("title")) %></strong></a><% end %>
106
+ </nav>
107
+ </main>
108
+
109
+ <aside class="toc" aria-label="On this page"><p>On this page</p>
110
+ <% if page["kind"] == "index" %>
111
+ <% index_groups.each do |group| %><a href="#<%= h(group.downcase.gsub(/[^a-z0-9]+/, "-")) %>"><%= h(group) %></a><% end %>
112
+ <% elsif page["kind"] == "markdown" %>
113
+ <% guide_toc.each do |anchor, heading| %><a href="#<%= h(anchor) %>"><%= heading %></a><% end %>
114
+ <% else %>
115
+ <% if page["kind"] == "component" %><a href="#import">Import</a><% end %>
116
+ <% page.fetch("sections", []).each do |section| %><a href="#<%= section_id(section) %>"><%= h(section.fetch("heading")) %></a><% end %>
117
+ <% if page["kind"] == "component" %><a href="#api">API summary</a><a href="#accessibility">Accessibility</a><% end %>
118
+ <% end %>
119
+ </aside>
120
+ </div>
121
+ <footer class="site-footer"><span>Zaniah is open source under the MIT License.</span><a href="https://github.com/noxdea/zaniah">Source on GitHub</a></footer>
122
+ </body>
123
+ </html>
@@ -0,0 +1,33 @@
1
+ # ADR 021: Represent dock layouts as a validated tree
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-24
5
+
6
+ ## Context
7
+
8
+ An application needs to save tabbed, split workspaces and restore them across
9
+ sessions. Dragging a tab changes both its group and potentially the split tree.
10
+ A fixed five-region dock cannot represent nested splits or stable panel identity.
11
+
12
+ ## Decision
13
+
14
+ `UI::DockLayout` stores tab groups and binary splits with stable string IDs.
15
+ `to_h` and `from_h` provide a plain-data round trip; parsing rejects duplicate
16
+ IDs, empty groups, invalid active tabs, invalid orientations, and out-of-range
17
+ ratios. `UI::DockWorkspace` derives visible panes from this model and reuses
18
+ `UI::SplitPane` for dividers. Layout changes produce a new validated model.
19
+ The application owns persistence and handles `on_detach` notifications; the
20
+ component does not create windows.
21
+
22
+ ## Alternatives considered
23
+
24
+ - A flat list of panels plus coordinates would make nested resizing and
25
+ restoration ambiguous.
26
+ - Extending `DockPanel` would change its simple five-region contract.
27
+ - Making detach automatically create windows would assume application policy.
28
+
29
+ ## Consequences
30
+
31
+ Persisted layouts can be validated independently of rendering. Empty groups
32
+ are pruned after a move. Applications can choose their own storage format and
33
+ window ownership; they must keep panel IDs stable across releases.
data/docs/components.md CHANGED
@@ -21,10 +21,21 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
21
21
  }
22
22
  ```
23
23
 
24
+ `VirtualTable`, `HexView`, and `TreeView` accept `viewport_height=` when a window
25
+ or split pane resizes. This updates their internal list viewport while retaining
26
+ scroll state and selection. `VirtualTable#follow_tail=` toggles bottom following;
27
+ call `scroll_to(source.count - 1, align: :end)` to resume immediately from elsewhere.
28
+
29
+ `TreeView#on_context_menu { |item, cx| menu }` selects the item under a right
30
+ click before opening the returned menu at the pointer. The callback receives the
31
+ same item value as `on_select`.
32
+
24
33
  ## Component reference
25
34
 
26
35
  | Layer | Component | Main constructor/options | Variants | Accessibility role |
27
36
  | --- | --- | --- | --- | --- |
37
+ | L4 | `VirtualTable` | `(source, columns:, height:, row_height:, selection:, follow_tail:)`; lazy `count`/`cell` source | stable row IDs, sortable/resizable headers, column visibility/order, row colors | table |
38
+ | L4 | `HexView` | `(bytes, bytes_per_row:, group:, offset_digits:, height:)`; `highlights=`, `select`, `scroll_to_offset` | viewport rows; synchronized hex/ASCII selection and copying | group |
28
39
  | L0 | `Label` | `(text, tone:, size:, wrap:)` | tone: default/muted/inverse; size: xs–xl | text |
29
40
  | L0 | `Icon` | `(source, size:, color:, label:)` | bundled: check/close/search/menu/info/warning | image when labeled |
30
41
  | L0 | `Divider` | `(axis:)` | horizontal/vertical | separator |
@@ -32,6 +43,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
32
43
  | L0 | `Card` | `(*children)`, `child` | theme surface | group |
33
44
  | L0 | `Badge` | `(text, variant:)` | neutral/accent/success/warning/danger | text |
34
45
  | L0 | `Kbd` | `(keys, platform:)`; `.for(action, keymap:)` | OS shortcut notation, including multi-stroke bindings | text |
46
+ | L0 | `Alert` | `(title, message:, variant:, action:, dismissible:, live:)`; `dismiss` | info/success/warning/danger; persistent until dismissed | alert when live, otherwise status |
35
47
  | L0 | `Avatar` | `(name, image:, size:)` | initials or PNG | image |
36
48
  | L0 | `Skeleton` | `(width:, height:)` | pulsing loading placeholder | progressbar/busy |
37
49
  | L0 | `EmptyState` | `(title, message:, icon:, action:)` | compositional | group |
@@ -42,6 +54,7 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
42
54
  | L1 | `Checkbox` | `(label, value:, disabled:)`; `on_change` | true/false/mixed | checkbox |
43
55
  | L1 | `Radio` | `(label, value:, disabled:)`; `on_change` | selected/unselected | radio |
44
56
  | L1 | `RadioGroup` | `(options, value:)`; `on_change` | one selected value | radiogroup |
57
+ | L1 | `SegmentedControl` | `(options, value:)`; `on_change` | mutually exclusive segments; arrows/Home/End | radiogroup/radio |
45
58
  | L1 | `Switch` | `(label, value:, disabled:)`; `on_change` | on/off | switch |
46
59
  | L1 | `Slider` | `(value:, min:, max:, step:, label:)`; `on_change` | pointer + arrow/Home/End/Page keys | slider |
47
60
  | L1 | `RangeSlider` | `(value: [low, high], ...)` | two thumbs | slider |
@@ -50,10 +63,11 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
50
63
  | L1 | `Meter` | `(value:, low:, high:, optimum:)` | thresholds | meter |
51
64
  | L2 | `Tooltip` | `(text, anchor:, side:, open:)` | top/bottom/left/right | tooltip |
52
65
  | L2 | `Popover` | `(content, anchor:, side:, width:, height:, open:, modal:)` | flipped and viewport-clamped | group |
66
+ | L2 | `HoverCard` | `(content, anchor:, open_delay:, close_delay:)` | hover or focus opens after a delay; non-modal | dialog |
53
67
  | L2 | `ContextMenu`, `Menu` | `(items, anchor:, open:)` | pointer + arrows/Home/End/Enter/Esc | menu/menuitem |
54
68
  | L2 | `MenuBar` | `(menus)` or `.from(app.menu_bar)` | declarative menu model or legacy pairs | menubar |
55
69
  | L2 | `Dropdown` | `(label, items:, value:)`; `on_change` | menu-backed | button |
56
- | L2 | `TextField` | `(value, placeholder:, label:, prefix:, suffix:, error:, max_length:, clearable:)` | IME, selection, counter | textbox |
70
+ | L2 | `TextField` | `(value, placeholder:, label:, prefix:, suffix:, error:, max_length:, clearable:)`; `status(kind, message:)`, `completion(provider)` | IME, selection, counter, validation, token completion | textbox |
57
71
  | L2 | `TextArea` | TextField plus `rows:` | multiline/wrapped | textbox/multiline |
58
72
  | L2 | `SearchInput` | TextField options | search + clear icons | searchbox |
59
73
  | L2 | `PasswordInput` | TextField options | masked display | textbox |
@@ -63,6 +77,8 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
63
77
  | L2 | `Combobox` | `(items, value:, label:, placeholder:, disabled:, matcher:)`; `on_change` | editable, filtered choices with highlighted matches | combobox |
64
78
  | L2 | `MultiSelect` | `(items, value:, label:, disabled:)`; `on_change` | multiple selected badges | listbox |
65
79
  | L2 | `DatePicker` | `(value, min:, max:, label:, disabled:)`; `on_change` | ISO date, day/week keyboard steps | combobox |
80
+ | L2 | `Calendar` | `(value:, min:, max:, range:, week_start:, month_names:)`; `on_change` | arrow keys move days/weeks, Page keys move months, Enter selects | grid/gridcell |
81
+ | L2 | `DateRangePicker` | `(value:, min:, max:, week_start:, month_names:)`; `on_change` | Calendar-backed start/end selection | combobox |
66
82
  | L2 | `TimePicker` | `(value, step:, label:, disabled:)`; `on_change` | 24-hour time, minute/hour keyboard steps | combobox |
67
83
  | L2 | `ColorPicker` | `(value, label:, swatches:, disabled:)`; `on_change` | hex input and swatches | combobox |
68
84
  | L3 | `Tabs` | `(items, selected:)`; `on_change` | arrows/Home/End | tab/tabpanel |
@@ -82,11 +98,14 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
82
98
  | L3 | `SplitPane` | `(first, second, orientation:, ratio:, min:, max:)`; `on_change` | horizontal/vertical, draggable separator | group/separator |
83
99
  | L3 | `PaneGrid` | `(panes, columns:, rows:, divider_size:, minimum:, keyboard_step:)`; `replace`, `on_resize` | arbitrary resizable grid, stable pane IDs | group/separator |
84
100
  | L3 | `Resizable` | `(content, width:, height:, min_width:, min_height:, max_width:, max_height:)`; `on_resize` | drag or keyboard resize | group/separator |
101
+ | L3 | `ZoomPanView` | `(content, zoom:, min_zoom:, max_zoom:)`; `fit`, `zoom_to`, `view_to_content` | drag to pan, pinch or Ctrl+wheel to zoom, `+`/`-`/`0` when focused | group |
85
102
  | L3 | `DockPanel` | `(center:, top:, right:, bottom:, left:)` | five-region layout | group |
103
+ | L3 | `DockWorkspace` | `(layout, render:)`; `on_layout_change`, `on_detach`; `DockLayout.to_h`/`.from_h` | drag tabs to move/split, arrows select, Alt+arrows move, Ctrl+Shift+arrows split, Ctrl+Shift+D requests detach | tablist/tab/tabpanel/separator |
86
104
  | L3 | `ListView` | `(items, height:, row_height:, selected:)`; `on_select` | virtual rows and keyboard selection | list/listitem |
87
105
  | 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
106
  | 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 |
89
- | L4 | `TreeView` | `(items, height:, selected:)`; `expand`, `collapse`, `replace`, `replace_children`, `invalidate`, lazy `children` proc | arrows/Home/End | tree/treeitem |
107
+ | L4 | `TreeView` | `(items, height:, selected:)`; `select_id`, `on_context_menu`, `expand`, `collapse`, `replace`, `replace_children`, `invalidate`, lazy `children` proc | arrows/Home/End | tree/treeitem |
108
+ | L4 | `PropertyGrid` | `(schema, values, height:, row_height:)`; `on_change`, `set` | typed existing controls, `Validation`, virtual rows | table/row/cell |
90
109
  | L5 | `Sparkline` | `(values, width:, height:, color:, label:)` | line + tooltip | image |
91
110
  | L5 | `LineChart`, `BarChart`, `StackedBarChart`, `AreaChart` | `(series, width:, height:, colors:, label:)`; `AreaChart(stacked:)` | shared axes, ticks, grid lines, color-keyed legend, tooltip | image |
92
111
  | L5 | `PieChart`, `DonutChart` | `(data, width:, height:, colors:, label:)` | slices, shared color-keyed legend, tooltip | image |
@@ -101,6 +120,28 @@ All input components are keyboard operable. Disabled controls remain visible but
101
120
  removed from focus traversal. Overlay components close on Esc; modal overlays restore
102
121
  the previous focus. See [TUI](tui.md) for terminal representations.
103
122
 
123
+ `Calendar` and `DateRangePicker` use Ruby's `Date` and ISO 8601 strings. Pass
124
+ `week_start: 0..6` (Sunday is 0) and twelve `month_names:` to localize the grid
125
+ without an i18n dependency. A range picker returns a two-element array of dates;
126
+ its second value is `nil` while the user is choosing the end. `HoverCard` accepts
127
+ an element/component as its anchor to open on pointer hover or keyboard focus, or
128
+ a `Point`/`Bounds` for positioned use. Its delays follow the window's injected clock.
129
+
130
+ `ZoomPanView` uses content-local coordinates for `zoom_to` and `view_to_content`.
131
+ Call `fit` after the first layout; it scales the content into the current viewport.
132
+ Native macOS pinch emits `Input::Magnify`, while Ctrl+wheel provides a desktop fallback.
133
+
134
+ `DockLayout` is a validated tree of tab groups and horizontal/vertical splits with
135
+ stable string IDs. Use `DockLayout.tabs` and `.split` to build it, and persist
136
+ `layout.to_h` in the application; `DockLayout.from_h` restores it. `DockWorkspace`
137
+ passes the active panel ID to `render:` and emits a new layout on tab changes.
138
+ `on_detach` is a notification only; the application decides whether to open a
139
+ window and remove the panel. A pointer drop in the outer 20% of a group splits it.
140
+ `PropertyGrid` schema entries use `key`, optional `label`, `type` (`text`,
141
+ `textarea`, `number`, `boolean`, `select`, `color`, `date`, `time`), `options` for
142
+ select, and an optional `Validation`. Invalid edits remain in the control while
143
+ the last valid value is retained. Both components expose terminal fallbacks.
144
+
104
145
  `Combobox` and `CommandPalette` default to case-insensitive substring matching in
105
146
  input order. Pass `matcher:` to either component, or set a default with
106
147
  `Zaniah.configure { |config| config.matcher = matcher }`. A matcher implements
@@ -169,6 +210,3 @@ resize. Grid and Table clipboard hooks receive half-open `Grid::Area` ranges;
169
210
  Table areas follow the current display order without changing its stable-ID
170
211
  selection. See [Layout](layout.md#two-axis-virtual-grid). Tree views use Up/Down to select, Right to expand or enter the first child,
171
212
  and Left to collapse or return to the parent.
172
-
173
- Run `bundle exec ruby tools/generate_component_gallery.rb` to rebuild the dark,
174
- light, and high-contrast component sheets plus overlay variants in all three themes.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/docs/inspection.md CHANGED
@@ -16,4 +16,6 @@ Zaniah::Inspection.perform(window, node, :press)
16
16
 
17
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
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).
19
+ The public API is declared in [`sig/inspection.rbs`](../sig/inspection.rbs).
20
+ Breaking changes receive at least one minor version of deprecation during 0.x,
21
+ as recorded in [ADR 013](adr/013-public-inspection-api.md).
data/docs/layout.md CHANGED
@@ -96,14 +96,6 @@ remains a set of stable row IDs; hook arguments are `Grid::Area` ranges in the
96
96
  current sorted display order, covering all columns. Disjoint selected rows
97
97
  produce separate areas. The application retains ownership of table data.
98
98
 
99
- `bench/grid.rb` measures a headless 800×600 viewport over a 1,000,000×16,000
100
- grid at 23 sequential scroll positions, including cell construction, layout,
101
- prepaint, and paint. `BUDGET=1 ruby bench/grid.rb` asserts a 16.67 ms median
102
- frame limit. A local run on 2026-09-23 measured 12.964 ms and constructed an
103
- average of 250 cells per frame. This microbenchmark does not include an
104
- application's backing store, expensive cell renderers, or a native compositor;
105
- rerun it on target hardware for deployment decisions.
106
-
107
99
  `ScrollView` accepts one child and clips it to the viewport. Its `axis` is
108
100
  `:vertical`, `:horizontal`, or `:both`; use `scroll_to` for programmatic
109
101
  scrolling and `scroll_state` for offsets and edge checks.