houseaccount 0.14.2 → 0.15.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +91 -151
  4. data/art/index.html.erb +37 -111
  5. data/config/locales/houseaccount.en.yml +0 -11
  6. data/lib/houseaccount/helpers/flows.rb +7 -39
  7. data/lib/houseaccount/helpers.rb +7 -5
  8. data/lib/houseaccount/version.rb +1 -1
  9. data/lib/houseaccount.rb +12 -4
  10. data/public/css/houseaccount.css +1 -1
  11. data/public/index.html +49 -117
  12. data/public/js/houseaccount.js +4 -4
  13. data/public/theme/bootstrap.css +2 -3
  14. data/public/{v0.14.2 → v0.15.0}/css/houseaccount.css +1 -1
  15. data/public/{v0.14.2 → v0.15.0}/js/houseaccount.js +4 -4
  16. data/public/{v0.14.2 → v0.15.0}/theme/bootstrap.css +2 -3
  17. metadata +33 -29
  18. data/lib/houseaccount/form_builder/comboboxes.rb +0 -38
  19. data/lib/houseaccount/form_builder/controls.rb +0 -67
  20. data/lib/houseaccount/form_builder/pins.rb +0 -30
  21. data/lib/houseaccount/form_builder.rb +0 -96
  22. data/lib/houseaccount/helpers/chats/asides.rb +0 -49
  23. data/lib/houseaccount/helpers/chats/asks.rb +0 -41
  24. data/lib/houseaccount/helpers/chats.rb +0 -42
  25. data/lib/houseaccount/helpers/dialogs.rb +0 -47
  26. data/lib/houseaccount/helpers/notices.rb +0 -55
  27. data/lib/houseaccount/message.rb +0 -11
  28. /data/public/{v0.14.2 → v0.15.0}/font/bootstrap-icons-FRBPO3H7.woff2 +0 -0
  29. /data/public/{v0.14.2 → v0.15.0}/font/quicksand-500-latin-QHYSRFVK.woff2 +0 -0
  30. /data/public/{v0.14.2 → v0.15.0}/font/quicksand-500-latin-ext-QZ4PXVHB.woff2 +0 -0
  31. /data/public/{v0.14.2 → v0.15.0}/font/quicksand-500-latin-ext.woff2 +0 -0
  32. /data/public/{v0.14.2 → v0.15.0}/font/quicksand-500-latin.woff2 +0 -0
  33. /data/public/{v0.14.2 → v0.15.0}/font/quicksand-500-vietnamese-RRA5CUQB.woff2 +0 -0
  34. /data/public/{v0.14.2 → v0.15.0}/font/quicksand-500-vietnamese.woff2 +0 -0
  35. /data/public/{v0.14.2 → v0.15.0}/theme/dawn.css +0 -0
  36. /data/public/{v0.14.2 → v0.15.0}/theme/dracula.css +0 -0
  37. /data/public/{v0.14.2 → v0.15.0}/theme/gruvbox.css +0 -0
  38. /data/public/{v0.14.2 → v0.15.0}/theme/monokai.css +0 -0
  39. /data/public/{v0.14.2 → v0.15.0}/theme/nord.css +0 -0
  40. /data/public/{v0.14.2 → v0.15.0}/theme/one_dark.css +0 -0
  41. /data/public/{v0.14.2 → v0.15.0}/theme/solarized.css +0 -0
  42. /data/public/{v0.14.2 → v0.15.0}/theme/tokyo_night.css +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49b64054d4b9cdaa0ce91c59af3b181618aa4f550343e08569d8ae60960c1833
4
- data.tar.gz: c01b758141861c04ab47f4fe1d63d028782c4e3193ecfdf2bde4668bb4c3661c
3
+ metadata.gz: 11c7aebc82ae0c5a776865022b6ee5f7672d6b9b9675cb952e2a46242f614e3c
4
+ data.tar.gz: b00c15bb33768c4a58330ca9cbe1554ca444dec05ab0181d07566fbe06fdc126
5
5
  SHA512:
6
- metadata.gz: acda012819638ddb27fa6c46d249b97d5bdc1e3547e42e2f06c07d284966fdf590fd70e7c3ca7e3d7bc74bf1c445aa04201b6af291207946744351f37f5b2b01
7
- data.tar.gz: 82e90aa97570fa13c9f848fd27d581dbc3096be2da07aac3f3afe2a46476dbeda63b569dd9f1c65aafee5f34e96cc627bc2387ebc7f98bf9a19f36836458eacf
6
+ metadata.gz: 27f473bea8654cb9d5867ca1776c343558eb2058753af333d6ce27cf66da64b7c96ff5fd9175219c245e649433d588f19a09e2603e72bcca0bc2accc95a50a8d
7
+ data.tar.gz: a047f95d97ce51af79385260a8f96ebef9977900fcfb05871940b0c860f1d096aebf6aebf2e51631466c716268d5f952b61dafcb2b83a642a796eaed2581b86d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 0.15.0 - 2026-09-18
11
+
12
+ * [Breaking change] The nine palettes move to bh as well
13
+
14
+ `dracula`, `nord`, `solarized` and the rest are editor colour schemes restating Bootstrap's
15
+ own families — nothing in them is this house's, which is the test that moved everything
16
+ else. They are bh's now, and the build copies them out of the gem to `public/theme/`, so
17
+ every URL a host has ever linked answers exactly as before.
18
+
19
+ * [Breaking change] Bootstrap 6 moves to the `bh` gem, which this one now depends on
20
+
21
+ Most of what had grown here was not the house's: a combobox, a 6-digit code, a dialog, a
22
+ toast, a thread of messages and twenty-two Stimulus controllers are Bootstrap 6 with the
23
+ markup written once, and are worth the same to any Rails app. They live in `bh` now, along
24
+ with the form builder, nine stylesheets and Bootstrap itself. This gem depends on it and
25
+ bundles it, so `css/houseaccount.css` and `js/houseaccount.js` still carry everything they
26
+ carried before and a host changes nothing.
27
+
28
+ What a host does have to change is where it reaches for a name. `HouseAccount::Message` is
29
+ `Bh::Message` and `HouseAccount::FormBuilder` is `Bh::FormBuilder` — both kept under the
30
+ house's names, so neither breaks — but the locale keys moved: `houseaccount.all`,
31
+ `.ask`, `.clear`, `.close`, `.more`, `.no_results`, `.okay`, `.search`, `.send`, `.typing`
32
+ and `.ago` are now `bh.*`, and a host that overrode one overrides it there. The meta that
33
+ words a confirmation's other button is `bh-cancel` rather than `houseaccount-cancel`.
34
+
35
+ * [Breaking change] `flow` takes what the page stands on rather than assuming it
36
+
37
+ bh's `flow` knows nothing of a lockup or a wall. The house's still draws both — `flow logo:`
38
+ is unchanged — but every other option is now the column's, so a page standing on something
39
+ of its own names the controller that paints it.
40
+
41
+ * [Breaking change] The npm package ships the built tree alone
42
+
43
+ It used to export `app/javascript` and `app/stylesheets` for a host that would rather
44
+ bundle the sources itself. Those sources now reach for bh, which is a gem and cannot be
45
+ resolved from `node_modules` — there is no npm package of bh and there will not be one —
46
+ so the exports go and the package is `public/css`, `public/js`, `public/font` and
47
+ `public/theme`: exactly what the CDN links in the README serve.
48
+
10
49
  ## 0.14.2 - 2026-09-18
11
50
 
12
51
  * [Fix] A conversation reads at the house's own size wherever it is drawn
data/README.md CHANGED
@@ -1,16 +1,22 @@
1
1
  # HouseAccount design
2
2
 
3
- Everything a HouseAccount page is made of, in one place: the stylesheet and the script every
4
- page loads, the marks every app is known by, the pages shown when an app is not, and the
5
- Rails helpers that draw a head, a logo or a chat the same way in every app.
3
+ What makes a page look like HouseAccount's: the marks every app is known by, the lockup they
4
+ sit in, the wall a signup flow stands on, the palettes, the pages an app shows when it is down
5
+ or lost, and the one stylesheet and one script that carry all of it.
6
+
7
+ The Bootstrap 6 underneath is [`bh`](https://claudiob.github.io/bh/), which this gem depends on
8
+ and bundles. **The components are documented there, not here** — the form builder, the combobox,
9
+ the 6-digit code, the phone field, the dialog, the toasts, the thread of messages and the
10
+ twenty-two Stimulus controllers are Bootstrap with the markup written once, and are worth the
11
+ same to any Rails app. This gem is only what is the house's.
6
12
 
7
13
  Published three ways from this repo:
8
14
 
9
15
  | Surface | Where | What it is for |
10
16
  | --- | --- | --- |
11
- | Site | https://design.houseaccount.com (staging: https://design.houseparty.dev) | A stable URL for every asset, and a showcase of all of them |
17
+ | Site | https://design.houseaccount.com (staging: https://design.houseparty.dev) | A stable URL for every asset, and a showcase of them |
12
18
  | Gem | `houseaccount` on RubyGems | A Rails engine: serves the same tree from the gem, and the helpers |
13
- | npm | `houseaccount` | The controllers and stylesheets as importable source |
19
+ | npm | `houseaccount` | The same built stylesheet and script, for any CDN that carries it |
14
20
 
15
21
  ## How to install
16
22
 
@@ -22,44 +28,13 @@ gem install houseaccount
22
28
 
23
29
  ```ruby
24
30
  # Gemfile
25
- gem 'houseaccount', '~> 0.14.0'
26
- ```
27
-
28
- `~> 0.14.0` stops short of `0.15`, since below 1.0 a minor release may break whatever it likes.
29
- Rails 8.1 and Ruby 3.2 are the minimum.
30
-
31
- ### From npm
32
-
33
- For a host that bundles the sources itself rather than serving the gem's built tree.
34
-
35
- ```bash
36
- npm install houseaccount
37
- ```
38
-
39
- ```js
40
- import 'houseaccount'
41
- import 'houseaccount/houseaccount.css'
31
+ gem 'houseaccount', '~> 0.15.0'
42
32
  ```
43
33
 
44
- The module exports nothing and is imported for its effect: it starts Stimulus, registers every
45
- controller the house's markup names, and hands Turbo the dialog it draws for a confirmation.
46
-
47
- Bootstrap 6 is not on npm, so the bundle ships inside this package and a bundler has to send the
48
- bare `bootstrap` specifier there:
34
+ bh comes with it; nothing else has to be named.
49
35
 
50
- ```js
51
- // esbuild
52
- alias: { bootstrap: 'houseaccount/bootstrap' }
53
- ```
54
-
55
- A single controller comes from `houseaccount/controllers`, for a host registering its own
56
- Stimulus application rather than taking the house's:
57
-
58
- ```js
59
- import WallController from 'houseaccount/controllers/wall_controller.js'
60
- ```
61
-
62
- Pin as a Gemfile does, and for the same reason: `"houseaccount": "~0.14.0"` stops short of `0.15`.
36
+ `~> 0.15.0` stops short of `0.16`, since below 1.0 a minor release may break whatever it likes.
37
+ Rails 8.1 and Ruby 3.2 are the minimum.
63
38
 
64
39
  A page takes the gem's layout, and wears the same head:
65
40
 
@@ -95,84 +70,68 @@ which Pages redirects away from.
95
70
 
96
71
  With the gem in the bundle an app serves `/css/houseaccount.css`, `/js/houseaccount.js`,
97
72
  `/theme/*`, `/font/*`, `/logo/*` and `/favicon/*` itself — and its own version's copy under
98
- `/v0.14.1/`, which is what the head helper links, cached for a year — answers a 400, 404, 406, 422 or 500
99
- with the house's own page, and has these helpers in every view:
73
+ `/v0.15.0/`, which is what the head helper links, cached for a year — answers a 400, 404, 406,
74
+ 422 or 500 with the house's own page, and answers `/down.html` with this app's.
75
+
76
+ ## Which app this is
77
+
78
+ Read off the Rails application's own name, the `Goldrush` of `Goldrush::Application`. That is
79
+ what its tab wears, what `houseaccount_head_tags` links without an argument, and whose page the
80
+ engine answers `/down.html` with — the app's mark in the middle and the house's at the foot. An
81
+ app the family has no mark for wears the cube.
82
+
83
+ ## The helpers
100
84
 
101
85
  ```erb
102
- <%# In the <head>: the viewport, color-scheme and Turbo refresh metas, the stylesheet, the script,
103
- and the icons a tab, a phone and a manifest ask for — this app's, by its name. %>
86
+ <%# In the <head>: the viewport, color-scheme and Turbo refresh metas, the stylesheet, the
87
+ script, and the icons a tab, a phone and a manifest ask for — this app's, by its name. %>
104
88
  <%= houseaccount_head_tags %>
105
89
 
106
90
  <%# The icons alone, for a head of the host's own: this app's unless another of the family is
107
- named, the way a page for providers wears the house's cube rather than the app's. A host of
108
- recourse needs no call at all — the gem answers recourse's `recourses/head` with them, so an
109
- admin's tab wears the app's cube the moment the gem is in the bundle. %>
91
+ named, the way a page for customers wears the house's cube rather than the app's. A host of
92
+ recourse needs no call at all — the gem answers recourse's `recourses/head` with them. %>
110
93
  <%= houseaccount_icons :houseaccount %>
111
94
 
112
- <%# A mark beside its name, as one word and one link. The house's cube and `houseaccount`
113
- leading to houseaccount.com by default, the way a page signs itself in a footer; any
114
- app's own with its name and wherever `href` says. Other options are attributes of the
115
- link. Under the cursor the whole logo turns the mark's blue. %>
95
+ <%# A mark beside its name, as one word and one link. Each drawing fills its 512 grid
96
+ differently, so equal sizes never align: every mark is rendered at a box of its own and
97
+ pushed down by its own nudge, and its ink then stands on the baseline. %>
116
98
  <%= houseaccount_logo %>
117
99
  <%= houseaccount_logo app: :fountain, href: root_path, class: 'fs-3' %>
118
100
 
119
- <%# A thread of messages, newest at the foot, and — where `url` is given — the field that
120
- posts the next one there, suggesting `hints` one at a time in its placeholder. %>
121
- <%= chat_with url: chat_question_path, hints: Provider::Chat::HINTS, messages: messages %>
122
-
123
- <%# What the flash says, as toasts at the bottom right: green for a notice, red for an alert,
124
- neutral for any other key; visible at first paint, gone after a moment or a click, held
125
- while read. A value in the flash that is not words is left alone. %>
126
- <%= notices %>
127
-
128
- <%# A link and the dialog it opens: headed, with an X, holding the block, and closed by an
129
- `Okay` too (`okay:` words it otherwise, `okay: false` leaves it out). The two share an id
130
- made from the link's words; any other option is an attribute of the link. %>
131
- <%= dialog 'I’m not on Google Business', title: 'Not a problem' do %>
132
- We will look your business up ourselves and match it by hand.
133
- <% end %>
134
-
135
- <%# The house's form, from `builder: HouseAccount::FormBuilder` or a controller's
136
- `default_form_builder`: `fieldset` a one-column grid with a legend where given, a label a
137
- form-label, a text or phone field a large control — the phone one shaped by the `phone`
138
- controller — a submit or a button the solid pill, a `type: :button` one the outlined pill, a
139
- disabled one given a `title` saying why on hover, and
140
- `pin_field` the six slots a code is typed into, one real field offered by the phone from the
141
- text that brought it. %>
142
- <%= form.fieldset do %>
143
- <%= form.label :pin_confirmation, '6-digit code' %>
144
- <%= form.pin_field :pin_confirmation, required: true %>
145
- <%= form.submit 'Confirm' %>
146
- <% end %>
147
-
148
101
  <%# An asset on the design site, for a page this app does not serve — a mail, say. %>
149
102
  <%= image_tag houseaccount_url('logo/fountain.png') %>
150
103
  ```
151
104
 
152
- Two more behaviors need no helper either, only an attribute. A `<select>` becomes Bootstrap's
153
- combobox — a toggle reading the pick, a menu with a search box, a check per pick where it
154
- is `multiple` and stays the one thing the form submits:
105
+ ## The layout
106
+
107
+ A page takes the gem's layout and wears the same head:
108
+
109
+ ```ruby
110
+ class Unauthenticated::AgentsController < ApplicationController
111
+ layout 'houseaccount'
112
+ end
113
+ ```
155
114
 
156
115
  ```erb
157
- <%= f.select :team_id, Team.pluck(:name, :id), { include_blank: 'None' },
158
- data: { controller: 'combobox', combobox_placeholder_value: 'Select…',
159
- search: 'Search…', clear: 'Clear search', no_results: 'No results found' } %>
116
+ <% content_for :title, 'Authenticating as admin' %>
117
+ <% content_for :head, tag.meta(name: 'robots', content: 'noindex') %>
118
+ <h1>Authenticating as admin</h1>
160
119
  ```
161
120
 
162
- On a `multiple` select, `data-combobox-all-value` names an `All` row that puts held-back
163
- options on the menu and `data-combobox-more-value` words the toggle when several are picked
164
- (`%{first} + %{count} more`); on an option, `data-count` puts a figure beside it and
165
- `data-hidden` holds it back until `All`. A pick is announced as the select's own `change`.
121
+ <p>
122
+ <img src='screenshot/layout-light.png' width='49%' alt='The page in light mode'>
123
+ <img src='screenshot/layout-dark.png' width='49%' alt='The page in dark mode'>
124
+ </p>
125
+
126
+ It is the charset, the title, the CSRF and CSP metas, `houseaccount_head_tags`, `yield :head`,
127
+ then `yield` inside a `main.container-fluid`, and `notices` after it — and nothing else. A host
128
+ wanting more copies it into its own `app/views/layouts/`, which wins on being earlier in the
129
+ view paths.
166
130
 
167
- And any link or form with `data-turbo-confirm` asks through a Bootstrap dialog the bundle
168
- draws on the first ask: the first line of the message is the question, the rest are
169
- paragraphs, and the answer wears the words of the button that asked. A host in another
170
- language names the other word in `<meta name='houseaccount-cancel'>`.
131
+ ## The flow, and the wall it stands on
171
132
 
172
- `flow` draws the page of a signup flow around a card: the mark over the wall, a title, a
173
- line under it, the card, and the way out at the foot. A layout of the app's own calls it and
174
- nests on the gem's layout, giving the page as `content_for :content` and rendering the gem's
175
- as a template:
133
+ `flow` is bh's page-of-a-signup-flow with the house's two things added: the lockup in its header
134
+ and the cyanotype behind the card. A layout of the app's own calls it and nests on the gem's:
176
135
 
177
136
  ```erb
178
137
  <% content_for :logo, 'houseaccount' %>
@@ -186,49 +145,23 @@ as a template:
186
145
  <%= render template: 'layouts/houseaccount' %>
187
146
  ```
188
147
 
189
- A form's controls sit in `form.fieldset`, a grid, with a `legend` over them where a page has
190
- one; `form.label` given both its words and a block wraps the control those words name, which is
191
- the cell a card wide enough for two columns lays out. Every kind of field is dressed by the
192
- builder rather than by the page — a number, a menu, a box that is ticked so a view names the
193
- kind it wants and no class at all, and `form.combobox` is a menu the bundle searches, given the
194
- words it says before a pick. Everything else in the fieldset keeps the whole row, and
195
- the card is what is measured rather than the window, so a narrow one stays in one column.
196
-
197
- `content_for :logo` is which of the family's marks the tab wears, for a page a customer reads
198
- rather than the app's own people: unsaid, it is this app's, by its name.
199
-
200
- The words are the view's, said in `content_for`: `:title` is the `h1`, `:subtitle` the line
201
- under it, drawn only where there is one, and `:width` how wide the card is, a CSS length that
202
- is `75rem` unless said — a page asking one question, a sign-in say, says `30rem`. What the layout knows goes in the call:
203
- `footer:` is the pieces of the foot, joined by a middot, none drawing no foot, and `logo:`
204
- where the mark links, the house's front door unless said.
205
-
206
- The wall alone is one attribute, `data-controller='wall'`, on any element that should
207
- stand on it.
208
-
209
- A message is anything answering six methods, and `HouseAccount::Message` is one for a host
210
- that has the facts and no model to hang them on:
211
-
212
- | Method | What it says |
213
- | --- | --- |
214
- | `side` | `:left` for the other party's bubble, `:right` for the reader's |
215
- | `text` | what was said — a string, or markup the host rendered; nil draws the typing dots |
216
- | `sender` | who said it, shown small above the bubble; nil shows nobody |
217
- | `at` | when, as `3 minutes ago` with the moment itself in a tooltip; nil shows no time |
218
- | `delivery` | `:delivered`, `:sent`, `:failed` or `:undelivered`, a ✓ or ✕ in its tone; nil for none |
219
- | `cost` | dollars the message cost, shown as `· $0.0123`; nil for none |
148
+ `logo:` is where the mark links, the house's front door unless said; `footer:` the pieces of the
149
+ foot, joined by a middot, none drawing no foot. Everything else `:title`, `:subtitle`,
150
+ `:width`, how the card lays its fields out is bh's, and is
151
+ [documented there](https://claudiob.github.io/bh/). `content_for :logo` is which of the family's
152
+ marks the tab wears, for a page a customer reads rather than the app's own people.
220
153
 
221
- ```ruby
222
- HouseAccount::Message.new side: :left, text: answer.note, sender: 'Claude', at: answer.created_at,
223
- cost: Chat.spent_on(answer.input_usage, answer.output_usage)
224
- ```
154
+ The wall alone is one attribute, `data-controller='wall'`, on any element that should stand on
155
+ it: fourteen trade tools and the family's five marks drifting up in seven columns, on blueprint
156
+ blues of its own so it reads the same whatever the primary is. Under `prefers-reduced-motion`
157
+ the columns stand still.
158
+
159
+ ## A code sent to a phone
225
160
 
226
- A code sent to a phone is the same words from every app, `HouseAccount.one_time_code`, in
227
- each voice they are said in. `written` is the sentence a person reads and, under it, the
228
- `@host #code` line that offers the code above the keyboard on the phone it arrived on;
229
- `spoken` is the sentence alone, its digits one at a time so a listener can write them down.
230
- The host is the application's default one unless `host:` says otherwise, and the words are
231
- the gem's `config/locales` entry.
161
+ The same words from every app, in each voice they are said in. `written` is the sentence a
162
+ person reads and, under it, the `@host #code` line that offers the code above the keyboard on
163
+ the phone it arrived on; `spoken` is the sentence alone, its digits one at a time so a listener
164
+ can write them down.
232
165
 
233
166
  ```ruby
234
167
  HouseAccount.one_time_code(contact.pin).written
@@ -237,6 +170,8 @@ HouseAccount.one_time_code(contact.pin).spoken
237
170
  # => "Enter this 6-digit code to proceed: 4, 8, 2, 9, 1, 3"
238
171
  ```
239
172
 
173
+ ## Off the gem
174
+
240
175
  A page not on the gem links the two files from the site, and nothing else:
241
176
 
242
177
  ```html
@@ -248,19 +183,19 @@ Those follow the latest release. Every release is also kept whole under its own
248
183
  forever and cached for a year, for a page that would rather move on its own terms:
249
184
 
250
185
  ```html
251
- <link rel='stylesheet' href='https://design.houseaccount.com/v0.14.1/css/houseaccount.css'>
252
- <script type='module' src='https://design.houseaccount.com/v0.14.1/js/houseaccount.js'></script>
186
+ <link rel='stylesheet' href='https://design.houseaccount.com/v0.15.0/css/houseaccount.css'>
187
+ <script type='module' src='https://design.houseaccount.com/v0.15.0/js/houseaccount.js'></script>
253
188
  ```
254
189
 
255
190
  The same two files come from any CDN that carries the npm package, for a page that would
256
191
  rather not depend on the site at all:
257
192
 
258
193
  ```html
259
- <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/houseaccount@0.14.1/public/css/houseaccount.css'>
260
- <script type='module' src='https://cdn.jsdelivr.net/npm/houseaccount@0.14.1/public/js/houseaccount.js'></script>
194
+ <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/houseaccount@0.15.0/public/css/houseaccount.css'>
195
+ <script type='module' src='https://cdn.jsdelivr.net/npm/houseaccount@0.15.0/public/js/houseaccount.js'></script>
261
196
  ```
262
197
 
263
- The palettes are beside them, at `public/theme/nord.css` and the rest. A published version is never
198
+ The palettes are beside them, at `public/theme/nord.css` and the rest — bh's nine, copied out of the gem at build time to the path hosts have always linked. A published version is never
264
199
  written again, so that URL is owed its bytes for as long as npm stands.
265
200
 
266
201
  A host with a Content Security Policy allows whichever origin it names for `style-src`,
@@ -268,13 +203,14 @@ A host with a Content Security Policy allows whichever origin it names for `styl
268
203
 
269
204
  ## What is in the bundle
270
205
 
271
- `css/houseaccount.css` is Bootstrap 6 alpha as upstream ships it, Bootstrap Icons cut down to
272
- the glyphs `art/icons.txt` names the ones `unicon` maps and the few written out by name —
273
- and then the house's rules by concern under `app/stylesheets/houseaccount/`. Quicksand, the
274
- lockup's face, is served from `/font` too; nothing on a page reaches another origin. `js/houseaccount.js` is
275
- Turbo (with the cable element `turbo-rails` signs its streams for), Bootstrap, and a Stimulus
276
- application with every controller the house's markup names, started once under
277
- `window.Stimulus`. Everything is drawn for light and dark alike: every color is a Bootstrap
206
+ `css/houseaccount.css` is bh's stylesheet — Bootstrap 6 alpha as upstream ships it, then bh's
207
+ own rules — followed by Bootstrap Icons cut down to the glyphs `art/icons.txt` names (the ones
208
+ `unicon` maps and the few written out by name) and the house's three: the page, the lockup and
209
+ the wall. Quicksand, the lockup's face, is served from `/font` too; nothing on a page reaches
210
+ another origin. `js/houseaccount.js` is bh's script Turbo (with the cable element
211
+ `turbo-rails` signs its streams for), Bootstrap, and a Stimulus application holding every
212
+ controller bh's markup names, started once under `window.Stimulus` and then `wall`, the one
213
+ controller that is the house's own. Everything is drawn for light and dark alike: every color is a Bootstrap
278
214
  token or a `light-dark()` pair, and `data-bs-theme` on `<html>` forces either. A phone is
279
215
  formatted in the browser: the server hands over ten digits and
280
216
  `data-controller='phone'`, and the controller decides `555-555-5555`.
@@ -289,6 +225,10 @@ rake icons # after editing art/icons.txt, the glyphs the icon fon
289
225
  cd test/dummy && bin/rails server
290
226
  ```
291
227
 
228
+ The build reads bh's sources out of the gem — `bundle info bh --path` — rather than out of
229
+ `node_modules`, so the Gemfile is the one place bh's version is pinned. An edit to bh shows in
230
+ this bundle on the next `rake assets`.
231
+
292
232
  The built `public/css`, `public/js`, `public/font` and `public/theme` are committed: the gem
293
233
  serves them, and `spec.files` is `git ls-files`. `rake assets` rebuilds and fails until the
294
234
  result is committed, so a source edit cannot ship without its build.
data/art/index.html.erb CHANGED
@@ -34,7 +34,7 @@
34
34
  <body class='text-center'>
35
35
  <header class='container-fluid px-3 py-3 border-bottom d-flex flex-wrap justify-content-center align-items-baseline gap-3'>
36
36
  <a class='lockup quicksand' href='#'><svg aria-hidden='true' style='width: 1.0515em; height: 1.0515em; top: 0.18625em' viewBox='0 0 512 512'><g fill='none' stroke='#3b8ae8' stroke-width='28' stroke-linecap='round' stroke-linejoin='round' transform='rotate(45 256 256)'><path d='M196 60 L316 60 L316 372 L196 372 Z'/><path d='M196 120 L316 120'/><path d='M196 372 L256 470 L316 372'/><path d='M232 431 L256 470 L280 431 Z' fill='#1B5288'/></g></svg>design</a>
37
- <nav class='fg-2'>the <a href='#theme'>themes</a>, <a href='#component'>components</a>, <a href='#page'>error pages</a>, <a href='#favicon'>icons</a> shared by <a href='https://pro.houseaccount.com/bookings' title='Fountain'><img class='design-mark' src='logo/fountain.svg' alt='Fountain'></a> <a href='https://gold.houseaccount.com/specialty_matches' title='Goldrush'><img class='design-mark' src='logo/goldrush.svg' alt='Goldrush'></a> <a href='https://providers.houseaccount.com/providers' title='Houston'><img class='design-mark' src='logo/houston.svg' alt='Houston'></a> <a href='https://checkout.houseaccount.com/providers' title='Autopilot'><img class='design-mark' src='logo/autopilot.svg' alt='Autopilot'></a></nav>
37
+ <nav class='fg-2'>the <a href='#lockup'>lockups</a>, <a href='#theme'>themes</a>, <a href='#page'>pages</a>, <a href='#favicon'>icons</a> shared by <a href='https://pro.houseaccount.com/bookings' title='Fountain'><img class='design-mark' src='logo/fountain.svg' alt='Fountain'></a> <a href='https://gold.houseaccount.com/specialty_matches' title='Goldrush'><img class='design-mark' src='logo/goldrush.svg' alt='Goldrush'></a> <a href='https://providers.houseaccount.com/providers' title='Houston'><img class='design-mark' src='logo/houston.svg' alt='Houston'></a> <a href='https://checkout.houseaccount.com/providers' title='Autopilot'><img class='design-mark' src='logo/autopilot.svg' alt='Autopilot'></a></nav>
38
38
  </header>
39
39
 
40
40
  <main class='container-fluid px-3'>
@@ -45,14 +45,14 @@
45
45
  <div class='card p-3'>
46
46
  <strong>Stylesheet</strong>
47
47
  <a class='design-url' href='css/houseaccount.css'><span class='d-none sm:d-inline'>https://design.houseaccount.com/</span>css/houseaccount.css</a>
48
- <span class='small fg-2 mt-1'>Bootstrap 6 with extra icons and components</span>
48
+ <span class='small fg-2 mt-1'>bh's Bootstrap 6, then the icon subset and the brand</span>
49
49
  </div>
50
50
  </div>
51
51
  <div class='col-12 md:col-6'>
52
52
  <div class='card p-3'>
53
53
  <strong>Script</strong>
54
54
  <a class='design-url' href='js/houseaccount.js'><span class='d-none sm:d-inline'>https://design.houseaccount.com/</span>js/houseaccount.js</a>
55
- <span class='small fg-2 mt-1'>Turbo, Bootstrap, and a Stimulus application</span>
55
+ <span class='small fg-2 mt-1'>bh's bundle, and the controller that paints the wall</span>
56
56
  </div>
57
57
  </div>
58
58
  <div class='col-12'>
@@ -74,117 +74,34 @@
74
74
  </button>
75
75
  </section>
76
76
 
77
- <section class='design-section' id='component'>
78
- <h2>Components</h2>
79
- <h3 class='h5 design-subsection'>Buttons and badges</h3>
80
- <div class='d-flex flex-wrap justify-content-center align-items-center gap-2'>
81
- <button type='button' class='btn btn-solid theme-primary'>Solid</button>
82
- <button type='button' class='btn btn-outline theme-primary'>Outline</button>
83
- <button type='button' class='btn btn-link p-0 align-baseline'>Link</button>
84
- <span class='badge'>enum</span>
85
- <span class='badge theme-success'>delivered</span>
86
- <span class='badge theme-danger'>failed</span>
87
- </div>
88
-
89
- <h3 class='h5 design-subsection'>Breadcrumb</h3>
90
- <nav aria-label='Breadcrumb'><ol class='breadcrumb m-0 justify-content-center'>
91
- <li class='breadcrumb-item'><a class='breadcrumb-link' href='#'><i class='bi bi-geo'></i> States</a></li>
92
- <li class='breadcrumb-item'><span class='breadcrumb-link active'>California</span></li>
93
- </ol></nav>
94
-
95
- <h3 class='h5 design-subsection' id='table'>Tables</h3>
96
- <div class='table-responsive' style='container-type: inline-size'>
97
- <table class='table table-hover sm:table-stacked'>
98
- <thead><tr>
99
- <th scope='col'><i class='bi bi-bookmark' role='img' data-controller='tooltip' data-bs-placement='top' data-bs-title='Kept'></i></th>
100
- <th scope='col'><a href='#'>Name <i class='bi bi-sort-down'></i></a></th>
101
- <th scope='col'><a href='#'>Phone</a></th>
102
- <th scope='col'><a href='#'>Status</a></th>
103
- <th scope='col' class='recourse-counter'><i class='bi bi-briefcase' role='img' data-controller='tooltip' data-bs-placement='top' data-bs-title='Jobs'></i></th>
104
- </tr></thead>
105
- <tbody>
106
- <tr class='recourse-kept'><td class='recourse-bookmark'><i class='bi bi-bookmark-fill'></i></td><td data-cell='Name'>Blue Crew</td><td data-cell='Phone'><span data-controller='phone'>4155550000</span></td><td data-cell='Status'><span class='badge'>active</span></td><td class='recourse-counter'><a href='#' data-cell='Jobs'><span class='recourse-counter-figure'>21</span><span class='recourse-counter-word'>21 jobs</span></a></td></tr>
107
- <tr class='recourse-written'><td class='recourse-bookmark'><i class='bi bi-bookmark'></i></td><td data-cell='Name'>Nashville Plumbing</td><td data-cell='Phone'><span data-controller='phone'>6155550123</span></td><td data-cell='Status'><span class='badge'>draft</span></td><td class='recourse-counter'><a href='#' data-cell='Jobs'><span class='recourse-counter-figure'>3</span><span class='recourse-counter-word'>3 jobs</span></a></td></tr>
108
- <tr><td class='recourse-bookmark'><i class='bi bi-bookmark'></i></td><td data-cell='Name'>Golden Gate Electric</td><td data-cell='Phone'><span data-controller='phone'>4155559876</span></td><td data-cell='Status'><span class='badge'>active</span></td><td class='recourse-counter'><a href='#' data-cell='Jobs'><span class='recourse-counter-figure'>38,405</span><span class='recourse-counter-word'>38,405 jobs</span></a></td></tr>
109
- </tbody>
110
- </table>
111
- </div>
112
-
113
- <h3 class='h5 design-subsection'>PIN</h3>
114
- <form class='mx-auto' onsubmit='return false' style='max-width: 22rem'>
115
- <div class='otp otp-lg' data-controller='otp' data-bs-otp='true'>
116
- <input class='otp-input' inputmode='numeric' pattern='[0-9]{6}' minlength='6' maxlength='6' autocomplete='one-time-code' required type='text' name='pin' id='pin' aria-label='6-digit code'>
117
- </div>
118
- </form>
119
-
120
- <h3 class='h5 design-subsection'>Combobox</h3>
121
- <div class='d-flex flex-wrap justify-content-center gap-3 align-items-start'>
122
- <div style='width: 16rem'>
123
- <label class='form-label' for='team'>Team</label>
124
- <select class='form-select' id='team' name='team' data-controller='combobox' data-combobox-placeholder-value='Select…' data-search='Search…' data-clear='Clear search' data-no-results='No results found'>
125
- <option value=''>None</option>
126
- <option value='1'>Blue Crew</option>
127
- <option value='2' selected>Golden Gate Electric</option>
128
- <option value='3'>Nashville Plumbing</option>
129
- </select>
130
- </div>
131
- <div style='width: 16rem'>
132
- <label class='form-label' for='markets'>Markets</label>
133
- <select class='form-select form-select-sm' id='markets' name='markets[]' multiple data-controller='combobox' data-combobox-placeholder-value='Market' data-combobox-all-value='All markets' data-combobox-more-value='%{first} + %{count} more' data-search='Search…' data-clear='Clear search' data-no-results='No results found'>
134
- <option value='1' data-count='38,405' selected>San Francisco</option>
135
- <option value='2' data-count='21' selected>Nashville</option>
136
- <option value='3' data-count='3'>Austin</option>
137
- <option value='4' data-count='0' data-hidden>Boise</option>
138
- </select>
139
- </div>
77
+ <section class='design-section' id='lockup'>
78
+ <h2>Lockups</h2>
79
+ <p class='fg-2'>A mark beside its name, as one word and one link. Each drawing fills its 512 grid
80
+ differently, so equal sizes never align: every mark is rendered at a box of its own and pushed
81
+ down by its own nudge, and its ink then stands on the baseline and reaches Quicksand's ascender.
82
+ The two blues are the mark's rather than the theme's, so a lockup reads the same in every app.</p>
83
+ <div class='d-flex flex-wrap justify-content-center align-items-baseline gap-4 mt-3'>
84
+ <%- mark.each_key do |app| -%>
85
+ <a class='lockup quicksand' href='logo/<%= app %>.svg'><%= mark[app] %><%- if app == :houseaccount -%><span class='house'>house</span>account<%- else -%><%= app %><%- end -%></a>
86
+ <%- end -%>
140
87
  </div>
141
-
142
- <h3 class='h5 design-subsection'>Confirm</h3>
143
- <turbo-frame id='confirm-demo'>
144
- <a class='btn btn-sm btn-solid theme-danger' href='#confirm-demo' data-turbo-method='get' data-turbo-frame='confirm-demo' data-turbo-confirm='Delete humanity?'>Delete state</a>
145
- </turbo-frame>
146
-
147
- <h3 class='h5 design-subsection'>Tooltip and relative time</h3>
148
- <p>Hover the icon and the time: <i class='bi bi-telephone' role='img' data-controller='tooltip' data-bs-placement='top' data-bs-title='Phone'></i> ·
149
- <time datetime='2026-09-13T12:00:00-04:00' data-controller='relative-time tooltip' data-bs-placement='left' data-bs-title='a moment ago'>Sep 13 at 12:00pm EDT</time></p>
150
-
151
- <h3 class='h5 design-subsection'>Phone</h3>
152
- <p>Phone: <span data-controller='phone'>4155550000</span></p>
153
- <div class='mx-auto' style='max-width: 20rem'>
154
- <label class='form-label' for='phone'>Phone</label>
155
- <input class='form-control' type='tel' id='phone' data-controller='phone' data-action='keydown->phone#down input->phone#input' value='4155550000'>
156
- </div>
157
-
158
- <h3 class='h5 design-subsection'>Mask</h3>
159
- <div class='form-control-plaintext d-flex justify-content-center gap-2 align-items-end' data-controller='reveal' data-reveal-plain-value='SEC-0000'>
160
- <span data-reveal-target='mask'>********</span>
161
- <button type='button' class='btn btn-link btn-sm p-0' data-action='reveal#show' data-reveal-target='button'>Show</button>
162
- </div>
163
-
164
- <h3 class='h5 design-subsection'>Toast</h3>
165
- <div class='toast show mx-auto' role='status' aria-live='polite' style='position: static'>
166
- <div class='toast-body d-flex align-items-center gap-2'><i class='bi bi-check-circle-fill fg-success'></i> Blue Crew was updated.</div>
88
+ <p class='fg-2 mt-4 mb-0'>At three sizes, to show the seating holds:</p>
89
+ <div class='d-flex flex-wrap justify-content-center align-items-baseline gap-4 mt-2'>
90
+ <a class='lockup quicksand' href='#lockup' style='font-size: 14px'><%= mark[:fountain] %>fountain</a>
91
+ <a class='lockup quicksand' href='#lockup'><%= mark[:fountain] %>fountain</a>
92
+ <a class='lockup quicksand' href='#lockup' style='font-size: 34px'><%= mark[:fountain] %>fountain</a>
167
93
  </div>
94
+ </section>
168
95
 
169
- <h3 class='h5 design-subsection' id='chat'>Chat</h3>
170
- <div class='chat-thread' data-controller='thread'>
171
- <div class='chat' data-thread-target='messages'>
172
- <small class='chat-aside chat-aside-out'>you <time datetime='2026-09-13T12:01:00-04:00' title='September 13, 2026 12:01'>a minute ago</time> <strong class='fg-success'>✓</strong></small>
173
- <p class='chat-bubble chat-out'>How much did I bill last month?</p>
174
- <small class='chat-aside chat-aside-in'><time datetime='2026-09-13T12:01:30-04:00' title='September 13, 2026 12:01'>a minute ago</time> · $0.0123</small>
175
- <p class='chat-bubble chat-in'>Across 31 jobs, up from $9,800 the month before.</p>
176
- <small class='chat-aside chat-aside-out'>you</small>
177
- <p class='chat-bubble chat-out'>And the month before?</p>
178
- <p class='chat-bubble chat-in chat-typing' aria-label='Typing'><span></span><span></span><span></span></p>
179
- </div>
180
- <form class='d-flex align-items-center' data-controller='require' action='#' onsubmit='return false'>
181
- <label for='ask' class='visually-hidden'>Ask a question</label>
182
- <input type='text' name='ask' id='ask' required autocomplete='off' class='form-control rounded-4 flex-grow-1 chat-ask'
183
- placeholder='How much did I bill last month?' data-thread-target='field' data-controller='placeholder'
184
- data-placeholder-questions-value='["How much did I bill last month?","Which ZIP codes book me most?","Which customers have booked me more than once?"]'>
185
- <input type='submit' value='Send' class='btn btn-solid rounded-3 theme-primary ms-3'>
186
- </form>
187
- </div>
96
+ <section class='design-section' id='component'>
97
+ <h2>Components</h2>
98
+ <p class='fg-2'>They are not this gem's. The form that dresses every field, the combobox, the
99
+ 6-digit code, the phone field, the dialog, the toasts, the thread of messages and the
100
+ twenty-two Stimulus controllers all live in <strong>bh</strong>, which is Bootstrap 6 for any
101
+ Rails app and has no opinion about this house. This gem depends on it and bundles it, so the
102
+ stylesheet and the script above already carry every one of them — and bh's own page draws each
103
+ beside the line of Ruby that produces it.</p>
104
+ <p class='mt-3'><a class='btn btn-solid theme-primary' href='https://claudiob.github.io/bh/'>Every component, on bh's page</a></p>
188
105
  </section>
189
106
 
190
107
  <section class='design-section' id='page'>
@@ -196,6 +113,15 @@
196
113
  <li><a href='page/422'>422 Unprocessable</a></li>
197
114
  <li><a href='page/500'>500 Server error</a></li>
198
115
  </ul>
116
+ <h3 class='h5 design-subsection'>Down</h3>
117
+ <p class='fg-2'>What an app shows while it is down — its own mark in the middle, the house's at
118
+ the foot. The engine answers <code>/down.html</code> with the right one, and Heroku's
119
+ <code>MAINTENANCE_PAGE_URL</code> points here, where the app is not.</p>
120
+ <ul class='list-unstyled d-flex flex-wrap justify-content-center gap-3'>
121
+ <%- mark.each_key do |app| -%>
122
+ <li><a href='down/<%= app %>'><%= app %></a></li>
123
+ <%- end -%>
124
+ </ul>
199
125
  </section>
200
126
 
201
127
  <section class='design-section' id='favicon'>
@@ -1,18 +1,7 @@
1
1
  en:
2
2
  houseaccount:
3
- ago: '%{time} ago'
4
- all: All
5
- ask: Ask a question
6
- clear: Clear
7
- close: Close
8
- more: '%{first} + %{count} more'
9
- no_results: Nothing matches that
10
- okay: Okay
11
3
  one_time_code: |-
12
4
  Enter this 6-digit code to proceed: %{pin}
13
5
 
14
6
  @%{host} #%{pin}
15
- search: Search
16
- send: Send
17
7
  spoken_one_time_code: 'Enter this 6-digit code to proceed: %{pin}'
18
- typing: Typing