rori 0.1.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +279 -0
- data/app/assets/stylesheets/rori/_init.css +79 -0
- data/app/assets/stylesheets/rori/layout.css +162 -0
- data/app/assets/stylesheets/rori/menubar.css +117 -0
- data/app/assets/stylesheets/rori/notifications.css +120 -0
- data/app/assets/stylesheets/rori/palette.css +114 -0
- data/app/assets/stylesheets/rori/shortcuts.css +110 -0
- data/app/assets/stylesheets/rori/terminal.css +98 -0
- data/app/assets/stylesheets/rori/themes.css +156 -0
- data/app/assets/stylesheets/rori/typing_badge.css +56 -0
- data/app/assets/stylesheets/rori/wallpaper.css +52 -0
- data/app/assets/stylesheets/rori/window.css +174 -0
- data/app/controllers/concerns/rori/windowed.rb +15 -0
- data/app/controllers/rori/commands/bars_controller.rb +9 -0
- data/app/controllers/rori/commands/runs_controller.rb +22 -0
- data/app/controllers/rori/commands/themes_controller.rb +9 -0
- data/app/controllers/rori/commands/ui_controller.rb +9 -0
- data/app/controllers/rori/commands/wallpapers_controller.rb +9 -0
- data/app/controllers/rori/commands_controller.rb +7 -0
- data/app/controllers/rori/desktops_controller.rb +7 -0
- data/app/helpers/rori/assets_helper.rb +9 -0
- data/app/helpers/rori/keymap_helper.rb +50 -0
- data/app/helpers/rori/prefs_helper.rb +13 -0
- data/app/helpers/rori/theme_helper.rb +14 -0
- data/app/helpers/rori/wallpaper_helper.rb +19 -0
- data/app/helpers/rori/window_helper.rb +36 -0
- data/app/javascript/rori/clock_controller.js +18 -0
- data/app/javascript/rori/command_index.js +84 -0
- data/app/javascript/rori/fuzzy.js +50 -0
- data/app/javascript/rori/index.js +21 -0
- data/app/javascript/rori/notifications_controller.js +45 -0
- data/app/javascript/rori/palette_controller.js +307 -0
- data/app/javascript/rori/prefs_controller.js +68 -0
- data/app/javascript/rori/rori_controller.js +996 -0
- data/app/javascript/rori/terminal_controller.js +198 -0
- data/app/javascript/rori/theme_controller.js +35 -0
- data/app/javascript/rori/wallpaper_controller.js +67 -0
- data/app/models/rori/command.rb +105 -0
- data/app/models/rori/notification.rb +20 -0
- data/app/models/rori/theme/color.rb +30 -0
- data/app/models/rori/theme.rb +87 -0
- data/app/models/rori/wallpaper.rb +54 -0
- data/app/views/layouts/rori/_head.html.haml +2 -0
- data/app/views/layouts/rori/shell.html.haml +97 -0
- data/app/views/layouts/rori/window.html.haml +10 -0
- data/app/views/rori/_menubar.html.haml +14 -0
- data/app/views/rori/_notification.html.haml +9 -0
- data/app/views/rori/_notifications.html.haml +10 -0
- data/app/views/rori/_palette.html.haml +13 -0
- data/app/views/rori/_shortcuts.html.haml +47 -0
- data/app/views/rori/_terminal.html.haml +9 -0
- data/app/views/rori/_wallpaper.html.haml +7 -0
- data/app/views/rori/_wallpaper_credit.html.haml +3 -0
- data/app/views/rori/_window.html.haml +15 -0
- data/app/views/rori/commands/_command.html.haml +17 -0
- data/app/views/rori/commands/index.html.haml +5 -0
- data/app/views/rori/desktops/show.html.haml +1 -0
- data/config/importmap.rb +3 -0
- data/config/locales/rori.en.yml +133 -0
- data/config/routes/rori.rb +13 -0
- data/lib/generators/rori/install/install_generator.rb +67 -0
- data/lib/generators/rori/install/templates/rori.rb.tt +22 -0
- data/lib/rori/engine.rb +19 -0
- data/lib/rori/version.rb +3 -0
- data/lib/rori.rb +180 -0
- data/lib/tasks/rori.rake +9 -0
- data/vendor/themes/LICENSE +25 -0
- data/vendor/themes/ghostty/Catppuccin Latte +22 -0
- data/vendor/themes/ghostty/Catppuccin Mocha +22 -0
- data/vendor/themes/ghostty/Everforest Dark Med +22 -0
- data/vendor/themes/ghostty/Flexoki Light +22 -0
- data/vendor/themes/ghostty/GitHub Dark Dimmed +22 -0
- data/vendor/themes/ghostty/Gruvbox Material Dark +22 -0
- data/vendor/themes/ghostty/Kanagawa Wave +22 -0
- data/vendor/themes/ghostty/Melange Dark +22 -0
- data/vendor/themes/ghostty/Nord +22 -0
- data/vendor/themes/ghostty/Rose Pine Dawn +22 -0
- data/vendor/themes/ghostty/Rose Pine Moon +22 -0
- data/vendor/themes/ghostty/TokyoNight Storm +22 -0
- data/vendor/themes/ghostty/Tomorrow Night Eighties +22 -0
- data/vendor/themes/ghostty/Zenbones Light +22 -0
- data/vendor/wallpapers/unsplash.yml +23 -0
- metadata +219 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 32d92384953e7fc7af8e9f2fc5e70aed475154cbb70b0e283419ec5e8516af47
|
|
4
|
+
data.tar.gz: a1a13b88264f163a1678ac6f576bb6564c53c888cb47a2536ac4efde460d4d71
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1dcbd2c85a36c19fce60a63a840ca7906f59884b525a3f70c862c409d57a3e369811c6e442f0fa3dd7577f44007e2d8d9dc859e8566fe5fbbc88aa0171fc6624
|
|
7
|
+
data.tar.gz: 8c4c1ba90c1ddd0a6aaeeb24de45a91fb793e82a480319a61c8ee8ebccf830b72d85461c4afe69c1abf28bbafb176628b1cfd29bad1c73bf81fe624c6d4d4582
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to rori, newest first. Each entry names who it's for:
|
|
4
|
+
**Users** (people working in the desk) and **Developers** (apps building on
|
|
5
|
+
it). History before the gem got its own repository lives in
|
|
6
|
+
[rori-demo's changelog](https://github.com/varyform/rori-demo/blob/main/CHANGELOG.md).
|
|
7
|
+
|
|
8
|
+
## 0.1.0 (2026-09-26)
|
|
9
|
+
|
|
10
|
+
The first release.
|
|
11
|
+
|
|
12
|
+
### The desk
|
|
13
|
+
- **Users:** every page opens as a window in endlessly scrolling column strips, one strip per workspace, stacked vertically: keyboard focus and moves, column widths, stacking, full width, centring, an overview and a column minimap. The layout survives a reload.
|
|
14
|
+
- **Users:** ⌘K palette and a drop-down terminal (`` ` ``) over one command tree — pages, recent records, desk actions, UI settings — with fuzzy matching across nested lists (`uthen` → UI › Theme › Nord), frecency, shortcuts shown next to commands, Tab completion and history in the terminal.
|
|
15
|
+
- **Users:** Blender-style hover keys (opt-in): bare keys act on the window under the pointer while the cursor stays in your field — `W` close, `U` reopen, `R` width, `F` full, `C` centre, `[` `]` stack, `1–9` switch workspace, `⇧1–9` move the column there and follow it.
|
|
16
|
+
- **Users:** a keyboard shortcuts modal (`⌥?`) generated from the live keymap; closed windows reopen where they were (`⌥⇧T`); unsaved forms ask before closing.
|
|
17
|
+
- **Users:** 14 bundled Ghostty colour themes with live preview; optional Unsplash wallpapers (safe / cover menu bar / off, next, pin); the menu bar at the top or bottom.
|
|
18
|
+
- **Users:** notifications in the desk's corner for server-side commands and background work.
|
|
19
|
+
|
|
20
|
+
### For host apps
|
|
21
|
+
- **Developers:** `bin/rails g rori:install` wires an app up: initializer, `Rori::Windowed`, `draw :rori`, the root route and the Stimulus registration.
|
|
22
|
+
- **Developers:** pages declare how they're shown (`window size:, mode:, workspace:, key:`); windows with the same key are reused; broadcast refreshes reload only the windows showing the changed data.
|
|
23
|
+
- **Developers:** `Rori.records` lists recent records in ⌘K; a labelled parameterless GET route becomes a command; `Rori.commands` adds entries; `Rori.command :name, confirm: true do … end` runs code on the server, and `Rori.notify` reports back from anywhere (e.g. a job).
|
|
24
|
+
- **Developers:** one keymap (`Rori.keymap`, `Mod` = `Rori.modifier`), a native-shell modifier by user agent (`Rori.native_user_agent`, `Rori.native_modifier`, `Rori.keymap_overrides`).
|
|
25
|
+
- **Developers:** self-contained CSS in one `rori` cascade layer with `--rori-*` tokens that fall back to the host's tokens; `rori-` prefixed classes; `Rori.themes_folder` and `Rori.wallpapers_folder` for the app's own themes and photos.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleh Khomei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Rori
|
|
2
|
+
|
|
3
|
+
[](https://github.com/varyform/rori/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A niri-style window manager for Rails pages — a "desk" of windows. Every
|
|
6
|
+
page opens as a window in endlessly scrolling column strips, one strip per workspace; ⌘K and a
|
|
7
|
+
drop-down terminal (`` ` ``) run one fuzzy-matched command tree; a keymap
|
|
8
|
+
with a configurable modifier (⌘ in a native shell) and optional hover keys
|
|
9
|
+
drive the windows. Ghostty themes and Unsplash wallpapers included.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
A Rails engine (not isolated): it wraps the host's own controllers and views.
|
|
14
|
+
|
|
15
|
+
See it wired into a small app in [rori-demo](https://github.com/varyform/rori-demo):
|
|
16
|
+
configuration, window options on pages, a server-side command with a
|
|
17
|
+
background job, and a Tauri macOS wrapper.
|
|
18
|
+
|
|
19
|
+
## A tour
|
|
20
|
+
|
|
21
|
+
**Windows in strips**: columns of any width, windows stacked inside them, the
|
|
22
|
+
strip running off the edges; one strip per workspace.
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
**⌘K** matches whole paths through nested lists: `uthen` finds UI › Theme › Nord.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
**The terminal** (`` ` ``) runs the same commands as words, with Tab completion
|
|
31
|
+
and history. Server-side commands can ask first and report back as
|
|
32
|
+
notifications.
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
**Overview** (⌥O) zooms out to every workspace.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
**Hover keys** (opt-in): point at an inactive window and bare keys act on it
|
|
41
|
+
(`W` closes, `R` cycles its width, `⇧1–9` sends it to a workspace) while the
|
|
42
|
+
cursor stays in the field you're typing in.
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
**Themes**: bundled Ghostty palettes, or your own theme files.
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
**Every shortcut** in one place (⌥?), generated from the live keymap.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
Rails 8 with Propshaft, importmap, Turbo and Stimulus.
|
|
57
|
+
|
|
58
|
+
Your app's views stay whatever they are — ERB, Phlex, anything. Only the
|
|
59
|
+
engine's own views are written in HAML, so the `haml` gem comes along as a
|
|
60
|
+
dependency; you never have to write a line of it.
|
|
61
|
+
|
|
62
|
+
> **Why HAML?** The desk's views are nothing but nesting, and HAML makes
|
|
63
|
+
> nesting the syntax: no closing tags, no towers of `<% end %>`. The ERB
|
|
64
|
+
> version had more angle brackets than a Rust signature with lifetimes —
|
|
65
|
+
> `fn render<'a, T: View + 'a>(v: &'a T) -> Result<Box<dyn Html + 'a>, Error>`
|
|
66
|
+
> — and nobody should have to read that at 2 a.m.
|
|
67
|
+
|
|
68
|
+
## Wiring it into an app
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Gemfile
|
|
72
|
+
gem "rori"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
bundle install
|
|
77
|
+
bin/rails g rori:install
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The generator adds `config/initializers/rori.rb`, includes `Rori::Windowed`
|
|
81
|
+
in `ApplicationController`, draws the desk's routes (and makes the blank desk
|
|
82
|
+
the root, unless you have one), and registers the Stimulus controllers. Run
|
|
83
|
+
it again any time; steps already in place are skipped. By hand, that's:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# app/controllers/application_controller.rb — every page renders as a window
|
|
87
|
+
class ApplicationController < ActionController::Base
|
|
88
|
+
include Rori::Windowed
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# config/routes.rb
|
|
92
|
+
Rails.application.routes.draw do
|
|
93
|
+
draw :rori # ⌘K / terminal command lists
|
|
94
|
+
root "rori/desktops#show" # the blank desk
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# config/initializers/rori.rb
|
|
98
|
+
Rori.configure do |rori|
|
|
99
|
+
rori.records = %w[ User Project ] # recent records listed in ⌘K
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// app/javascript/controllers/index.js
|
|
105
|
+
import { registerRori } from "rori"
|
|
106
|
+
registerRori(application)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Pages declare how they're shown with `window size:, mode:, workspace:, key:`
|
|
110
|
+
(see `Rori::WindowHelper`); links that should open a window use
|
|
111
|
+
`rori_link_to`. A parameterless GET route shows up in ⌘K once it has a
|
|
112
|
+
label under `rori.commands.routes.<controller>.<action>` in the app's locale.
|
|
113
|
+
|
|
114
|
+
## CSS
|
|
115
|
+
|
|
116
|
+
The desk's CSS lives in its own cascade layer, `rori` (sub-layers
|
|
117
|
+
`rori.reset`, `rori.tokens`, `rori.base`, `rori.layout`, `rori.components`,
|
|
118
|
+
`rori.themes`), so it never mixes into the host's layers. Place it among
|
|
119
|
+
yours with one name:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
@layer reset, base, rori, layout, components, utilities;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Unmentioned, it lands after your layers (it loads later).
|
|
126
|
+
|
|
127
|
+
It needs nothing from the host: its components use only `--rori-*` tokens,
|
|
128
|
+
each falling back from a host token when present — `--color-canvas`,
|
|
129
|
+
`--color-surface`, `--color-ink`, `--color-ink-muted`, `--color-line`,
|
|
130
|
+
`--color-primary`, `--color-on-primary`, `--color-success`, `--color-danger`,
|
|
131
|
+
`--color-hover`, `--color-backdrop`, `--gap`, `--radius`, `--radius-sm`,
|
|
132
|
+
`--ease`, `--motion`, `--font-sans`, `--font-mono` — to a built-in default.
|
|
133
|
+
Themes set those host tokens, so they re-colour the host's pages as well.
|
|
134
|
+
|
|
135
|
+
## What the host provides
|
|
136
|
+
|
|
137
|
+
- **Page styles** (buttons, forms, tables); the desk styles only its chrome.
|
|
138
|
+
- **Head tags** (favicons…): override `app/views/layouts/rori/_head.html.haml`.
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
See `lib/rori.rb` for everything: `app_name`, `records`, `commands`, `keymap`,
|
|
143
|
+
`modifier` / `native_modifier` / `native_user_agent`, `hover_keys`,
|
|
144
|
+
`terminal_key`, `wallpapers`, theme and wallpaper files and cookies.
|
|
145
|
+
|
|
146
|
+
### Records in ⌘K
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Rori.configure do |rori|
|
|
150
|
+
rori.records = %w[ User Project ]
|
|
151
|
+
rori.record_limit = 25 # the default
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Each time ⌘K opens (and when the terminal loads its commands), the desk lists
|
|
156
|
+
the `record_limit` most recently updated records of every model named here,
|
|
157
|
+
so typing “ada” jumps straight to that user. An entry is labelled with
|
|
158
|
+
`record.to_s`, grouped by `Model.model_name.human`, and opens
|
|
159
|
+
`polymorphic_path(record)` in a window. Each model therefore needs:
|
|
160
|
+
|
|
161
|
+
- an `updated_at` column (the list is ordered by it);
|
|
162
|
+
- a show route, e.g. `resources :users` (`polymorphic_path` raises without one);
|
|
163
|
+
- a meaningful `to_s`, or the label reads `#<User:0x…>`.
|
|
164
|
+
|
|
165
|
+
It's a shortcut to recent work, not a search: older records don't appear, and
|
|
166
|
+
each model costs one query per open. Leaving a model out only drops its
|
|
167
|
+
records from ⌘K; its pages still open from links, and its index can still be
|
|
168
|
+
listed through a route label (`rori.commands.routes.<controller>.index`).
|
|
169
|
+
|
|
170
|
+
### Server-side commands and notifications
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
Rori.configure do |rori|
|
|
174
|
+
rori.command :reindex_search, confirm: true do
|
|
175
|
+
SearchReindexJob.perform_later
|
|
176
|
+
I18n.t("search.reindex_started") # optional: the notification's text (default "Done")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
en:
|
|
183
|
+
rori:
|
|
184
|
+
commands:
|
|
185
|
+
custom:
|
|
186
|
+
reindex_search: Reindex search
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The command shows up in ⌘K and the terminal under **Run**. Picking it POSTs
|
|
190
|
+
to `/rori/commands/runs`, runs the block and shows the result as a corner
|
|
191
|
+
notification (errors stay until dismissed). `confirm: true` asks first: ↵
|
|
192
|
+
twice in ⌘K, `y` in the terminal. The block runs inside the request, so
|
|
193
|
+
hand slow work to a job, which can report back from anywhere:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
Rori.notify "Search reindexed", "1,204 records", kind: :success # :info, :success, :error
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`Rori.notify` broadcasts over Action Cable to `Rori.notifications_stream`,
|
|
200
|
+
which every open desk subscribes to — all users see it, so keep it to
|
|
201
|
+
single-user or admin desks.
|
|
202
|
+
|
|
203
|
+
### Your own themes and wallpapers
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
Rori.configure do |rori|
|
|
207
|
+
rori.themes_folder = "config/themes" # Ghostty theme files (relative to Rails.root)
|
|
208
|
+
rori.wallpapers = true
|
|
209
|
+
rori.wallpapers_folder = "wallpapers" # app/assets/images/wallpapers/*.{jpg,png,webp,avif}
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- **Themes:** any Ghostty theme file (e.g. from
|
|
214
|
+
[iTerm2-Color-Schemes/ghostty](https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/ghostty))
|
|
215
|
+
dropped into the folder shows up in UI › Theme next to the bundled ones; a
|
|
216
|
+
file with a bundled theme's name replaces it. No build step — their CSS is
|
|
217
|
+
rendered into the page.
|
|
218
|
+
- **Wallpapers:** the folder is a path inside the app's asset load path, so
|
|
219
|
+
images are fingerprinted and served by Propshaft. When set, its images
|
|
220
|
+
replace the bundled Unsplash photos.
|
|
221
|
+
- UI › Wallpaper › **Next wallpaper** swaps the photo in place; **Pin
|
|
222
|
+
wallpaper** keeps the current one across launches (toggle).
|
|
223
|
+
|
|
224
|
+
The bundled themes are compiled into the gem's `themes.css`; after changing
|
|
225
|
+
`Rori.themes_directory`, run `bin/rails rori:themes:build`.
|
|
226
|
+
|
|
227
|
+
## Customising the look
|
|
228
|
+
|
|
229
|
+
The desk's chrome reads only its own `--rori-*` tokens, and each one defaults
|
|
230
|
+
to your token of the same meaning. So there are three levels, from broad to
|
|
231
|
+
precise:
|
|
232
|
+
|
|
233
|
+
1. **Your design tokens** — set them as usual; the desk follows:
|
|
234
|
+
```css
|
|
235
|
+
:root { --radius: 4px; --radius-sm: 2px; --gap: 8px; --font-sans: "Inter", sans-serif; }
|
|
236
|
+
```
|
|
237
|
+
Supported: `--color-canvas|surface|ink|ink-muted|line|primary|on-primary|success|danger|hover|backdrop`,
|
|
238
|
+
`--gap`, `--radius`, `--radius-sm`, `--ease`, `--motion`, `--font-sans`, `--font-mono`.
|
|
239
|
+
2. **Desk-only tokens** — change the desk without touching your pages:
|
|
240
|
+
```css
|
|
241
|
+
@layer overrides { /* any layer AFTER `rori`, or unlayered */
|
|
242
|
+
:root { --rori-radius: 0; --rori-gap: 16px; }
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
3. **Components** — every chrome element has a `rori-` class
|
|
246
|
+
(`.rori-win`, `.rori-win__bar`, `.rori-menubar`, `.rori-minimap`,
|
|
247
|
+
`.rori-palette`, `.rori-terminal`, `.rori-col`…):
|
|
248
|
+
```css
|
|
249
|
+
@layer overrides {
|
|
250
|
+
.rori-win { border-radius: 0; box-shadow: none; }
|
|
251
|
+
.rori-win__bar { height: 26px; }
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**The one rule: overrides must come after the `rori` layer.** Cascade layers
|
|
256
|
+
beat specificity, so a rule in a layer *before* `rori` loses even with a more
|
|
257
|
+
specific selector — and so does a `--rori-*` token set there, since
|
|
258
|
+
`rori.tokens` defines them on `:root` too. Put overrides in a later layer
|
|
259
|
+
(`components`, `utilities`, a dedicated `overrides`) or leave them unlayered.
|
|
260
|
+
Overriding your own tokens (level 1) works from any layer, because the desk
|
|
261
|
+
only reads them.
|
|
262
|
+
|
|
263
|
+
Themes set the `--color-*` tokens, so a theme picked in UI › Theme wins over
|
|
264
|
+
level-1 colours; use level 2 (`--rori-ink`, `--rori-surface`…) for colours a
|
|
265
|
+
theme mustn't change.
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
bundle install
|
|
271
|
+
bundle exec rake test # models, controllers, the generator, against test/dummy
|
|
272
|
+
bundle exec rubocop
|
|
273
|
+
bin/rails server # the dummy app, for poking at the desk
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
To work on the gem inside a real app, use the GitHub source in the app's
|
|
277
|
+
Gemfile (`gem "rori", github: "varyform/rori", branch: "main"`) and point
|
|
278
|
+
Bundler at your checkout: `bundle config set --local local.rori /path/to/rori`.
|
|
279
|
+
Releases: see `RELEASING.md`.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* The desk's CSS lives in one `desk` layer with its own sub-layers, so it never
|
|
2
|
+
mixes into the host's layers. Hosts position it with one name, e.g.
|
|
3
|
+
@layer reset, base, rori, layout, components, utilities;
|
|
4
|
+
Unmentioned, it lands after the host's layers (it loads later). Keep it
|
|
5
|
+
after the host's token layer: desk.themes overrides the host's colours. */
|
|
6
|
+
@layer rori {
|
|
7
|
+
@layer reset, tokens, base, layout, components, themes;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Rori tokens: the host's semantic tokens when it has them, standalone
|
|
11
|
+
defaults otherwise. Components use only --rori-*. Computed on :root, so
|
|
12
|
+
themes (which set the host names on :root) flow through. */
|
|
13
|
+
@layer rori.tokens {
|
|
14
|
+
:root {
|
|
15
|
+
--rori-gap: var(--gap, 12px);
|
|
16
|
+
--rori-radius: var(--radius, 10px);
|
|
17
|
+
--rori-radius-sm: var(--radius-sm, 6px);
|
|
18
|
+
--rori-ease: var(--ease, cubic-bezier(0.2, 0.8, 0.2, 1));
|
|
19
|
+
--rori-motion: var(--motion, 320ms);
|
|
20
|
+
--rori-font-sans: var(--font-sans, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
21
|
+
--rori-font-mono: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
|
|
22
|
+
|
|
23
|
+
--rori-canvas: var(--color-canvas, light-dark(oklch(0.95 0 0), oklch(0.14 0 0)));
|
|
24
|
+
--rori-surface: var(--color-surface, light-dark(oklch(0.995 0 0), oklch(0.195 0 0)));
|
|
25
|
+
--rori-ink: var(--color-ink, light-dark(oklch(0.2 0 0), oklch(0.94 0 0)));
|
|
26
|
+
--rori-ink-muted: var(--color-ink-muted, color-mix(in oklch, var(--rori-ink) 60%, var(--rori-surface)));
|
|
27
|
+
--rori-line: var(--color-line, color-mix(in oklch, var(--rori-ink) 14%, var(--rori-surface)));
|
|
28
|
+
--rori-primary: var(--color-primary, var(--rori-ink));
|
|
29
|
+
--rori-on-primary: var(--color-on-primary, var(--rori-surface));
|
|
30
|
+
--rori-success: var(--color-success, light-dark(oklch(0.55 0.12 150), oklch(0.74 0.1 150)));
|
|
31
|
+
--rori-danger: var(--color-danger, light-dark(oklch(0.55 0.16 27), oklch(0.7 0.13 25)));
|
|
32
|
+
--rori-hover: var(--color-hover, color-mix(in oklch, var(--rori-ink) 6%, transparent));
|
|
33
|
+
--rori-backdrop: var(--color-backdrop, color-mix(in oklch, oklch(0 0 0) 45%, transparent));
|
|
34
|
+
--rori-shadow: var(--shadow, 0 8px 24px color-mix(in oklch, oklch(0 0 0) 22%, transparent));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Just enough reset for the desk's own chrome, whatever the host resets.
|
|
39
|
+
:where() keeps it at zero specificity; window content is left to the host. */
|
|
40
|
+
@layer rori.reset {
|
|
41
|
+
:where(.rori-menubar, .rori-viewport, .rori-palette, .rori-terminal, .rori-shortcuts, .rori-notifications) :where(*, *::before, *::after) {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:where(.rori-menubar, .rori-palette, .rori-terminal, .rori-win__bar, .rori-wallpaper, .rori-shortcuts, .rori-notifications) :where(button, input:not([type="checkbox"]), ul, p, dl, dd, label) {
|
|
46
|
+
margin: 0;
|
|
47
|
+
padding: 0;
|
|
48
|
+
border: 0;
|
|
49
|
+
border-radius: 0;
|
|
50
|
+
background: none;
|
|
51
|
+
color: inherit;
|
|
52
|
+
font: inherit;
|
|
53
|
+
list-style: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:where(.rori-menubar, .rori-viewport, .rori-palette, .rori-terminal) [hidden] {
|
|
57
|
+
display: none !important;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* The shell owns the page: its type and colours come from the tokens (so from
|
|
62
|
+
the host's, when set). */
|
|
63
|
+
@layer rori.base {
|
|
64
|
+
html {
|
|
65
|
+
background: var(--rori-canvas);
|
|
66
|
+
color: var(--rori-ink);
|
|
67
|
+
font: 13px/1.45 var(--rori-font-sans);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
body {
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:where(.rori-menubar, .rori-palette, .rori-terminal, .rori-wallpaper) kbd {
|
|
75
|
+
padding: 0 4px;
|
|
76
|
+
border: 1px solid var(--rori-line);
|
|
77
|
+
font: 12px var(--rori-font-mono);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
@layer rori.layout {
|
|
2
|
+
|
|
3
|
+
/* minmax(0, 1fr): an implicit `auto` column would grow to the widest bar's
|
|
4
|
+
content (e.g. the unwrapped status hint) and push the viewport past the
|
|
5
|
+
window on narrow or zoomed screens, leaving focused columns cut off. */
|
|
6
|
+
/* A two-finger swipe that overscrolls the page is history navigation in
|
|
7
|
+
Chrome and Safari ("back" on a leftward swipe). Sideways swipes belong to
|
|
8
|
+
the strip, and the desk manages windows itself, so the page never lets
|
|
9
|
+
overscroll through — nor pull-to-refresh or rubber-banding. */
|
|
10
|
+
html,
|
|
11
|
+
body {
|
|
12
|
+
overscroll-behavior: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
height: 100vh;
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-rows: 32px 1fr;
|
|
19
|
+
grid-template-columns: minmax(0, 1fr);
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Rows are assigned explicitly: other body children (turbo-cable-stream-source,
|
|
24
|
+
a host's own elements) fall into implicit rows after them instead of
|
|
25
|
+
taking the bar's row. */
|
|
26
|
+
.rori-menubar {
|
|
27
|
+
grid-row: 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.rori-viewport {
|
|
31
|
+
grid-row: 2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* UI › Menu bar › Bottom. */
|
|
35
|
+
body.bars-bottom {
|
|
36
|
+
grid-template-rows: 1fr 32px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
body.bars-bottom .rori-menubar {
|
|
40
|
+
grid-row: 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
body.bars-bottom .rori-viewport {
|
|
44
|
+
grid-row: 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Workspaces stack vertically; each is an endless horizontal strip of columns.
|
|
48
|
+
rori_controller.js sets column widths and the strip/stack transforms. */
|
|
49
|
+
/* clip, not hidden: dialog.show() focuses the window and would scroll a
|
|
50
|
+
hidden-overflow box behind the transforms' back. */
|
|
51
|
+
.rori-viewport {
|
|
52
|
+
position: relative;
|
|
53
|
+
overflow: clip;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.rori-stack {
|
|
57
|
+
position: absolute;
|
|
58
|
+
inset: 0;
|
|
59
|
+
transform-origin: 0 0;
|
|
60
|
+
transition: transform var(--rori-motion) var(--rori-ease);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rori-workspace {
|
|
64
|
+
height: 100%;
|
|
65
|
+
overflow: clip;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.is-overview .rori-workspace {
|
|
69
|
+
overflow: visible;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.rori-strip {
|
|
73
|
+
display: flex;
|
|
74
|
+
gap: var(--rori-gap);
|
|
75
|
+
width: max-content;
|
|
76
|
+
height: 100%;
|
|
77
|
+
padding: var(--rori-gap);
|
|
78
|
+
transition: transform var(--rori-motion) var(--rori-ease);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.rori-col {
|
|
82
|
+
flex: none;
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: var(--rori-gap);
|
|
86
|
+
min-width: 0;
|
|
87
|
+
transition: width var(--rori-motion) var(--rori-ease);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Loading windows wait here unseen; modals live here in the top layer. */
|
|
91
|
+
.rori-floating {
|
|
92
|
+
position: absolute;
|
|
93
|
+
inset: 0;
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.rori-floating>.rori-win:not(:modal) {
|
|
98
|
+
display: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.is-booting :is(.rori-stack, .rori-strip, .rori-col),
|
|
102
|
+
.is-scrolling .rori-strip {
|
|
103
|
+
transition: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.is-overview .rori-workspace {
|
|
107
|
+
outline: 1px dashed var(--rori-line);
|
|
108
|
+
outline-offset: -1px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.is-overview .rori-win {
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.is-overview .rori-win__body {
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* A centred panel, so the dismiss button sits by the text. Above the (empty)
|
|
120
|
+
stack so the button takes clicks; the panel itself lets them through. */
|
|
121
|
+
.rori-wallpaper {
|
|
122
|
+
position: absolute;
|
|
123
|
+
z-index: 1;
|
|
124
|
+
inset: 50% auto auto 50%;
|
|
125
|
+
translate: -50% -50%;
|
|
126
|
+
display: grid;
|
|
127
|
+
gap: 6px;
|
|
128
|
+
padding: 14px 36px;
|
|
129
|
+
border-radius: var(--rori-radius);
|
|
130
|
+
text-align: center;
|
|
131
|
+
color: var(--rori-ink-muted);
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.rori-viewport:not(.is-empty) .rori-wallpaper,
|
|
136
|
+
.hint-hidden .rori-wallpaper {
|
|
137
|
+
display: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.rori-wallpaper__dismiss {
|
|
141
|
+
position: absolute;
|
|
142
|
+
top: 6px;
|
|
143
|
+
right: 6px;
|
|
144
|
+
width: 22px;
|
|
145
|
+
height: 22px;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
color: var(--rori-ink-muted);
|
|
148
|
+
line-height: 1;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
pointer-events: auto;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.rori-wallpaper__dismiss:hover {
|
|
154
|
+
background: var(--rori-hover);
|
|
155
|
+
color: var(--rori-ink);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.rori-wallpaper__title {
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
color: var(--rori-ink);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
@layer rori.components {
|
|
2
|
+
|
|
3
|
+
/* The desk's only bar: brand, workspaces, the column minimap (takes the
|
|
4
|
+
slack), search, shortcuts, clock. */
|
|
5
|
+
.rori-menubar {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 12px;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
padding: 0 var(--rori-gap);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.rori-menubar .rori-minimap {
|
|
14
|
+
flex: 1;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.rori-menubar__shortcuts {
|
|
20
|
+
flex: none;
|
|
21
|
+
color: var(--rori-ink-muted);
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.rori-menubar__shortcuts:hover {
|
|
28
|
+
color: var(--rori-ink);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.rori-menubar__brand {
|
|
32
|
+
font: 600 12px var(--rori-font-mono);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.rori-menubar__search {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 24px;
|
|
39
|
+
height: 22px;
|
|
40
|
+
min-width: 18rem;
|
|
41
|
+
padding: 0 4px 0 10px;
|
|
42
|
+
border: 1px solid var(--rori-line);
|
|
43
|
+
border-radius: var(--rori-radius-sm);
|
|
44
|
+
color: var(--rori-ink-muted);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.rori-menubar__search:hover {
|
|
49
|
+
border-color: var(--rori-ink-muted);
|
|
50
|
+
color: var(--rori-ink);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.rori-menubar__search kbd {
|
|
54
|
+
margin-left: auto;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.rori-menubar__clock {
|
|
58
|
+
font: 12px var(--rori-font-mono);
|
|
59
|
+
color: var(--rori-ink-muted);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.rori-workspaces {
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 4px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.rori-workspace-button {
|
|
68
|
+
height: 18px;
|
|
69
|
+
padding: 0 7px;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
font: 11px var(--rori-font-mono);
|
|
72
|
+
color: var(--rori-ink-muted);
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.rori-workspace-button:hover {
|
|
78
|
+
background: var(--rori-hover);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.rori-workspace-button[aria-current] {
|
|
82
|
+
background: var(--rori-primary);
|
|
83
|
+
color: var(--rori-on-primary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.rori-minimap {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 4px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.rori-minimap__col {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 2px;
|
|
96
|
+
height: 20px;
|
|
97
|
+
padding: 2px;
|
|
98
|
+
border: 1px solid transparent;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition: width var(--rori-motion) var(--rori-ease);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.rori-minimap__col.is-visible {
|
|
105
|
+
border-color: var(--rori-line);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.rori-minimap__col span {
|
|
109
|
+
flex: 1;
|
|
110
|
+
border-radius: 2px;
|
|
111
|
+
background: var(--rori-line);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.rori-minimap__col span.is-focused {
|
|
115
|
+
background: var(--rori-ink);
|
|
116
|
+
}
|
|
117
|
+
}
|