houseaccount 0.3.0 → 0.4.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +11 -5
  4. data/app/views/layouts/houseaccount.html.erb +24 -0
  5. data/app/views/recourses/_head.html.erb +1 -0
  6. data/art/index.html.erb +298 -0
  7. data/lib/houseaccount/helpers/heads.rb +11 -10
  8. data/lib/houseaccount/version.rb +1 -1
  9. data/public/404.html +118 -0
  10. data/public/index.html +2 -2
  11. data/public/js/houseaccount.js +1 -1
  12. data/public/{v0.3.0 → v0.4.0}/css/houseaccount.css +1 -1
  13. data/public/{v0.3.0 → v0.4.0}/js/houseaccount.js +1 -1
  14. metadata +23 -19
  15. /data/public/{v0.3.0 → v0.4.0}/font/bootstrap-icons-6LILWPE2.woff2 +0 -0
  16. /data/public/{v0.3.0 → v0.4.0}/font/quicksand-500-latin-QHYSRFVK.woff2 +0 -0
  17. /data/public/{v0.3.0 → v0.4.0}/font/quicksand-500-latin-ext-QZ4PXVHB.woff2 +0 -0
  18. /data/public/{v0.3.0 → v0.4.0}/font/quicksand-500-latin-ext.woff2 +0 -0
  19. /data/public/{v0.3.0 → v0.4.0}/font/quicksand-500-latin.woff2 +0 -0
  20. /data/public/{v0.3.0 → v0.4.0}/font/quicksand-500-vietnamese-RRA5CUQB.woff2 +0 -0
  21. /data/public/{v0.3.0 → v0.4.0}/font/quicksand-500-vietnamese.woff2 +0 -0
  22. /data/public/{v0.3.0 → v0.4.0}/theme/bootstrap.css +0 -0
  23. /data/public/{v0.3.0 → v0.4.0}/theme/dawn.css +0 -0
  24. /data/public/{v0.3.0 → v0.4.0}/theme/dracula.css +0 -0
  25. /data/public/{v0.3.0 → v0.4.0}/theme/gruvbox.css +0 -0
  26. /data/public/{v0.3.0 → v0.4.0}/theme/monokai.css +0 -0
  27. /data/public/{v0.3.0 → v0.4.0}/theme/nord.css +0 -0
  28. /data/public/{v0.3.0 → v0.4.0}/theme/one_dark.css +0 -0
  29. /data/public/{v0.3.0 → v0.4.0}/theme/solarized.css +0 -0
  30. /data/public/{v0.3.0 → v0.4.0}/theme/tokyo_night.css +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 939b9379b9f9074c36dfafeaa68f37c91154a36976cd64025170653a71b1918a
4
- data.tar.gz: 552f0b7b147cb46cb7edab29c293a6d570cd936f6198028694364f8de9f895c0
3
+ metadata.gz: 319d9ebb96bf0d727fc55eae76537b3dc1b7020f1998a3166fe8b9abf3a9d46b
4
+ data.tar.gz: 2ab4ff9df10a0bfebf212d00d3fb72926fe55e98fb151953e46172740a4c70f8
5
5
  SHA512:
6
- metadata.gz: 0da5799f9fcfbb25157745e1641dbbace60b62d40eb344774ea123e2e909f08d912384810b1d9500a5c17d16ad7b17323f5a8fdd7e04ed5907d861a4e9adfb82
7
- data.tar.gz: 06e33a1b81ca80e1ccc595be50bdfcc64b4756cda9e1de9741251e91183e5d742a62ed552c22d4017e1149f40de5196947d63afef0d1ee3026e761315f34396e
6
+ metadata.gz: 611605798bfb399f19931914a1487e128a193c3e006c33a5771e18775fa20a4a035f526f1e9616ed02bc5611ba61776b8b8eedef210a4cb438d5a57ae4e9e0e1
7
+ data.tar.gz: 0661a634ec3280f1a6d1d53fddb7c7df37035cb4f34d35fac7bc705a69edd99ca5e809675930b8374a1a8b509dc2500a04a0059a8e3f09d4b181578201f155da
data/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 0.4.0 - 2026-09-14
11
+
12
+ * [Feature] `houseaccount_icons` names an app's icons on its own, and answers recourse's head
13
+
14
+ The four icon links `houseaccount_head_tags` wrote are a helper of their own, for a head
15
+ the host draws itself, taking another app of the family by name where a page should wear
16
+ the house's cube rather than the app's. The gem also ships `recourses/_head.html.erb`
17
+ calling it, so a host of recourse 5.1 wears its own cube on every admin tab with nothing
18
+ written.
19
+
20
+ * [Fix] The site answers a missing path with the not-found page, not the showcase
21
+
22
+ Cloudflare Pages serves `index.html` at 200 for any path without a file unless a root
23
+ `404.html` exists; a stylesheet asked for at a version the site did not carry came back
24
+ as HTML. The branded not-found page now sits at the root as well.
25
+
26
+ * [Fix] A chat's field takes focus without scrolling the page to it
27
+
28
+ The showcase opened scrolled down to its chat, since the thread controller focused the
29
+ field on load and the browser followed. Focus no longer scrolls; a chat page is sized to
30
+ the window, so its field is in view regardless.
31
+
32
+ ## 0.3.1 - 2026-09-14
33
+
34
+ * [Fix] The gem ships its layout
35
+
36
+ 0.3.0 left `app/views` out of the gem along with the sources the build compiles, so a
37
+ host asking for `layout 'houseaccount'` found no template. The layout ships; the
38
+ JavaScript, stylesheet and font sources still stay behind.
39
+
10
40
  ## 0.3.0 - 2026-09-14
11
41
 
12
42
  * [Fix] A chat scrolls inside its card, and the page stands still
data/README.md CHANGED
@@ -20,10 +20,10 @@ gem install houseaccount
20
20
 
21
21
  ```ruby
22
22
  # Gemfile
23
- gem 'houseaccount', '~> 0.3.0'
23
+ gem 'houseaccount', '~> 0.4.0'
24
24
  ```
25
25
 
26
- `~> 0.3.0` stops short of `0.4`, since below 1.0 a minor release may break whatever it likes.
26
+ `~> 0.4.0` stops short of `0.5`, since below 1.0 a minor release may break whatever it likes.
27
27
  Rails 8.1 and Ruby 3.2 are the minimum.
28
28
 
29
29
  A page takes the gem's layout, and wears the same head:
@@ -60,7 +60,7 @@ which Pages redirects away from.
60
60
 
61
61
  With the gem in the bundle an app serves `/css/houseaccount.css`, `/js/houseaccount.js`,
62
62
  `/theme/*`, `/font/*`, `/logo/*` and `/favicon/*` itself — and its own version's copy under
63
- `/v0.3.0/`, which is what the head helper links, cached for a year — answers a 400, 404, 406, 422 or 500
63
+ `/v0.4.0/`, which is what the head helper links, cached for a year — answers a 400, 404, 406, 422 or 500
64
64
  with the house's own page, and has these helpers in every view:
65
65
 
66
66
  ```erb
@@ -68,6 +68,12 @@ with the house's own page, and has these helpers in every view:
68
68
  and the icons a tab, a phone and a manifest ask for — this app's, by its name. %>
69
69
  <%= houseaccount_head_tags %>
70
70
 
71
+ <%# The icons alone, for a head of the host's own: this app's unless another of the family is
72
+ named, the way a page for providers wears the house's cube rather than the app's. A host of
73
+ recourse needs no call at all — the gem answers recourse's `recourses/head` with them, so an
74
+ admin's tab wears the app's cube the moment the gem is in the bundle. %>
75
+ <%= houseaccount_icons :houseaccount %>
76
+
71
77
  <%# A mark beside its name, as one word and one link. The house's cube and `houseaccount`
72
78
  leading to houseaccount.com by default, the way a page signs itself in a footer; any
73
79
  app's own with its name and wherever `href` says. Other options are attributes of the
@@ -190,8 +196,8 @@ Those follow the latest release. Every release is also kept whole under its own
190
196
  forever and cached for a year, for a page that would rather move on its own terms:
191
197
 
192
198
  ```html
193
- <link rel='stylesheet' href='https://design.houseaccount.com/v0.3.0/css/houseaccount.css'>
194
- <script type='module' src='https://design.houseaccount.com/v0.3.0/js/houseaccount.js'></script>
199
+ <link rel='stylesheet' href='https://design.houseaccount.com/v0.4.0/css/houseaccount.css'>
200
+ <script type='module' src='https://design.houseaccount.com/v0.4.0/js/houseaccount.js'></script>
195
201
  ```
196
202
 
197
203
  A host with a Content Security Policy allows that origin for `style-src`, `script-src` and
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+ <head>
4
+ <meta charset='utf-8'>
5
+ <title><%= content_for(:title) || HouseAccount.app %></title>
6
+ <%- if protect_against_forgery? -%>
7
+ <meta name='csrf-param' content='<%= request_forgery_protection_token %>'>
8
+ <meta name='csrf-token' content='<%= form_authenticity_token %>'>
9
+ <%- end -%>
10
+ <%- if (csp = csp_meta_tag) -%>
11
+ <%= csp %>
12
+ <%- end -%>
13
+ <%= houseaccount_head_tags %>
14
+ <%- if content_for?(:head) -%>
15
+ <%= yield :head %>
16
+ <%- end -%>
17
+ </head>
18
+ <body class='min-vh-100 d-flex flex-column'>
19
+ <main class='container-fluid py-4 flex-grow-1 d-flex flex-column'>
20
+ <%= content_for?(:content) ? yield(:content) : yield %>
21
+ </main>
22
+ <%= notices %>
23
+ </body>
24
+ </html>
@@ -0,0 +1 @@
1
+ <%= houseaccount_icons %>
@@ -0,0 +1,298 @@
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+ <head>
4
+ <meta charset='utf-8'>
5
+ <title>HouseAccount design</title>
6
+ <meta name='viewport' content='width=device-width, initial-scale=1'>
7
+ <meta name='color-scheme' content='light dark'>
8
+ <meta name='description' content='Every asset a HouseAccount app loads, and every component the houseaccount gem draws, in one place.'>
9
+ <link rel='icon' href='favicon/design.svg' type='image/svg+xml'>
10
+ <link rel='icon' href='favicon/design-96.png' type='image/png' sizes='96x96'>
11
+ <link rel='apple-touch-icon' href='favicon/design-180.png'>
12
+ <link rel='manifest' href='site.webmanifest'>
13
+ <!-- The same two files every HouseAccount page loads, by relative path so this page
14
+ reads the same on staging, on production and from a folder on disk. -->
15
+ <link rel='stylesheet' href='css/houseaccount.css'>
16
+ <link rel='stylesheet' href='theme/bootstrap.css' data-recourse-theme=''>
17
+ <script type='module' src='js/houseaccount.js'></script>
18
+ <style>
19
+ /* The page's own chrome and nothing else: everything shown below is styled by
20
+ css/houseaccount.css alone, which is the point of showing it. */
21
+ .design-section { padding-block: 2rem; border-block-end: var(--bs-border-width) solid var(--bs-border-color); }
22
+ .design-swatch { display: flex; flex-direction: column; align-items: center; gap: .5rem; width: 7rem; }
23
+ .design-swatch img { width: 4rem; height: 4rem; }
24
+ .design-url { font-family: var(--bs-font-monospace); font-size: var(--bs-font-size-sm); color: var(--bs-fg-2); overflow-wrap: anywhere; }
25
+ .design-shell { height: 18rem; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-radius-3); overflow: hidden; }
26
+ .design-shell .recourse-shell { height: 100%; }
27
+ .chat-thread { max-width: 36rem; }
28
+ /* The wall fills a viewport; here it fills a box, so the page can show it beside the rest. */
29
+ .design-wall { position: relative; height: 24rem; overflow: hidden; border-radius: var(--bs-radius-3); }
30
+ .design-wall .tools { position: absolute; }
31
+ .design-wall .flow { height: 100%; justify-content: center; }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <header class='container-fluid px-3 py-3 border-bottom d-flex align-items-center gap-3'>
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
+ <span class='fg-2'>the front end every HouseAccount app shares</span>
38
+ <nav class='ms-auto d-flex gap-3 small'>
39
+ <a href='#logo'>Logos</a><a href='#favicon'>Favicons</a><a href='#theme'>Palettes</a>
40
+ <a href='#component'>Components</a><a href='#chat'>Chat</a><a href='#wall'>Wall</a><a href='#table'>Tables</a><a href='#page'>Pages</a>
41
+ </nav>
42
+ </header>
43
+
44
+ <main class='container-fluid px-3'>
45
+ <section class='design-section'>
46
+ <h1 class='quicksand'>Everything a HouseAccount page is made of</h1>
47
+ <p class='lead' style='max-width: 48rem'>
48
+ One stylesheet, one script, the marks every app is known by, and the pages shown when an
49
+ app is not. Each is at a stable path here; an app on the <code>houseaccount</code> gem
50
+ serves the same paths itself, and everything else links them from this origin.
51
+ </p>
52
+ <div class='row g-3' style='max-width: 60rem'>
53
+ <div class='col-12 md:col-6'>
54
+ <div class='card p-3'>
55
+ <strong>Stylesheet</strong>
56
+ <span class='design-url'>https://design.houseaccount.com/css/houseaccount.css</span>
57
+ <span class='small fg-2 mt-1'>Bootstrap 6 alpha, Bootstrap Icons and the house's own rules.</span>
58
+ </div>
59
+ </div>
60
+ <div class='col-12 md:col-6'>
61
+ <div class='card p-3'>
62
+ <strong>Script</strong>
63
+ <span class='design-url'>https://design.houseaccount.com/js/houseaccount.js</span>
64
+ <span class='small fg-2 mt-1'>Turbo, Bootstrap, and a Stimulus application with every controller the house names.</span>
65
+ </div>
66
+ </div>
67
+ <div class='col-12'>
68
+ <div class='card p-3'>
69
+ <strong>Pinned to a version — the latest is <%= version %></strong>
70
+ <span class='design-url'>https://design.houseaccount.com/v<%= version %>/css/houseaccount.css · /v<%= version %>/js/houseaccount.js · /v<%= version %>/theme/nord.css</span>
71
+ <span class='small fg-2 mt-1'>Every release stays at its own prefix forever, whole and cached for a year, for a page that moves on its own terms. The unversioned paths follow the latest.</span>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </section>
76
+
77
+ <section class='design-section' id='logo'>
78
+ <h2>Logos</h2>
79
+ <p class='fg-2'>One drawing per app under <code>/logo</code>, as SVG and as a 512-pixel PNG.</p>
80
+ <div class='d-flex flex-wrap gap-4'>
81
+ <figure class='design-swatch'><img src='logo/houseaccount.svg' alt='HouseAccount'><figcaption class='design-url'>logo/houseaccount.svg</figcaption></figure>
82
+ <figure class='design-swatch'><img src='logo/fountain.svg' alt='Fountain'><figcaption class='design-url'>logo/fountain.svg</figcaption></figure>
83
+ <figure class='design-swatch'><img src='logo/goldrush.svg' alt='Goldrush'><figcaption class='design-url'>logo/goldrush.svg</figcaption></figure>
84
+ <figure class='design-swatch'><img src='logo/houston.svg' alt='Houston'><figcaption class='design-url'>logo/houston.svg</figcaption></figure>
85
+ <figure class='design-swatch'><img src='logo/autopilot.svg' alt='Autopilot'><figcaption class='design-url'>logo/autopilot.svg</figcaption></figure>
86
+ <figure class='design-swatch'><img src='logo/design.svg' alt='Design'><figcaption class='design-url'>logo/design.svg</figcaption></figure>
87
+ <figure class='design-swatch'><img src='logo/houseaccount.png' alt='HouseAccount, bitmap'><figcaption class='design-url'>logo/houseaccount.png</figcaption></figure>
88
+ </div>
89
+ </section>
90
+
91
+ <section class='design-section' id='favicon'>
92
+ <h2>Favicons</h2>
93
+ <p class='fg-2'>What a tab, a phone and a manifest each ask for, per app under <code>/favicon</code>. The pink one is a local machine's.</p>
94
+ <div class='table-responsive'>
95
+ <table class='table table-hover' style='max-width: 60rem'>
96
+ <thead><tr><th scope='col'>App</th><th scope='col'>.svg</th><th scope='col'>-dev.svg</th><th scope='col'>-96.png</th><th scope='col'>-180.png</th><th scope='col'>-192.png</th><th scope='col'>-512.png</th><th scope='col'>.ico</th><th scope='col'>.webmanifest</th></tr></thead>
97
+ <tbody>
98
+ <tr><th scope='row'>houseaccount</th><td><img src='favicon/houseaccount.svg' width='24' alt=''></td><td><img src='favicon/houseaccount-dev.svg' width='24' alt=''></td><td><img src='favicon/houseaccount-96.png' width='24' alt=''></td><td><img src='favicon/houseaccount-180.png' width='24' alt=''></td><td><img src='favicon/houseaccount-192.png' width='24' alt=''></td><td><img src='favicon/houseaccount-512.png' width='24' alt=''></td><td><a href='favicon/houseaccount.ico'>ico</a></td><td><a href='favicon/houseaccount.webmanifest'>manifest</a></td></tr>
99
+ <tr><th scope='row'>fountain</th><td><img src='favicon/fountain.svg' width='24' alt=''></td><td><img src='favicon/fountain-dev.svg' width='24' alt=''></td><td><img src='favicon/fountain-96.png' width='24' alt=''></td><td><img src='favicon/fountain-180.png' width='24' alt=''></td><td><img src='favicon/fountain-192.png' width='24' alt=''></td><td><img src='favicon/fountain-512.png' width='24' alt=''></td><td><a href='favicon/fountain.ico'>ico</a></td><td><a href='favicon/fountain.webmanifest'>manifest</a></td></tr>
100
+ <tr><th scope='row'>goldrush</th><td><img src='favicon/goldrush.svg' width='24' alt=''></td><td><img src='favicon/goldrush-dev.svg' width='24' alt=''></td><td><img src='favicon/goldrush-96.png' width='24' alt=''></td><td><img src='favicon/goldrush-180.png' width='24' alt=''></td><td><img src='favicon/goldrush-192.png' width='24' alt=''></td><td><img src='favicon/goldrush-512.png' width='24' alt=''></td><td><a href='favicon/goldrush.ico'>ico</a></td><td><a href='favicon/goldrush.webmanifest'>manifest</a></td></tr>
101
+ <tr><th scope='row'>houston</th><td><img src='favicon/houston.svg' width='24' alt=''></td><td><img src='favicon/houston-dev.svg' width='24' alt=''></td><td><img src='favicon/houston-96.png' width='24' alt=''></td><td><img src='favicon/houston-180.png' width='24' alt=''></td><td><img src='favicon/houston-192.png' width='24' alt=''></td><td><img src='favicon/houston-512.png' width='24' alt=''></td><td><a href='favicon/houston.ico'>ico</a></td><td><a href='favicon/houston.webmanifest'>manifest</a></td></tr>
102
+ <tr><th scope='row'>design</th><td><img src='favicon/design.svg' width='24' alt=''></td><td><img src='favicon/design-dev.svg' width='24' alt=''></td><td><img src='favicon/design-96.png' width='24' alt=''></td><td><img src='favicon/design-180.png' width='24' alt=''></td><td><img src='favicon/design-192.png' width='24' alt=''></td><td><img src='favicon/design-512.png' width='24' alt=''></td><td><a href='favicon/design.ico'>ico</a></td><td><a href='favicon/design.webmanifest'>manifest</a></td></tr>
103
+ <tr><th scope='row'>autopilot</th><td><img src='favicon/autopilot.svg' width='24' alt=''></td><td><img src='favicon/autopilot-dev.svg' width='24' alt=''></td><td><img src='favicon/autopilot-96.png' width='24' alt=''></td><td><img src='favicon/autopilot-180.png' width='24' alt=''></td><td><img src='favicon/autopilot-192.png' width='24' alt=''></td><td><img src='favicon/autopilot-512.png' width='24' alt=''></td><td><a href='favicon/autopilot.ico'>ico</a></td><td><a href='favicon/autopilot.webmanifest'>manifest</a></td></tr>
104
+ </tbody>
105
+ </table>
106
+ </div>
107
+ </section>
108
+
109
+ <section class='design-section' id='theme'>
110
+ <h2>Palettes</h2>
111
+ <p class='fg-2'>Nine stylesheets under <code>/theme</code> that repaint Bootstrap's ramps; the toggle rotates through them and flips the mode, the way the sidebar's does.</p>
112
+ <div class='d-flex align-items-center gap-3'>
113
+ <button type='button' class='btn btn-outline theme-primary' data-controller='scheme' data-action='scheme#rotate'
114
+ data-scheme-themes-value='["bootstrap","dawn","dracula","gruvbox","monokai","nord","one_dark","solarized","tokyo_night"]'
115
+ data-scheme-path-value='theme' data-scheme-storage-value='recourse-scheme'>
116
+ <span class='recourse-scheme-light'><i class='bi bi-moon-fill'></i> Another palette, dark</span>
117
+ <span class='recourse-scheme-dark'><i class='bi bi-sun-fill'></i> Another palette, light</span>
118
+ </button>
119
+ <span class='design-url'>theme/&lt;name&gt;.css · bootstrap dawn dracula gruvbox monokai nord one_dark solarized tokyo_night</span>
120
+ </div>
121
+ </section>
122
+
123
+ <section class='design-section' id='component'>
124
+ <h2>Components</h2>
125
+ <p class='fg-2'>Bootstrap 6 as the house draws it. Every rule that differs from upstream is in <code>css/houseaccount.css</code>.</p>
126
+
127
+ <h3 class='h5 mt-4'>Buttons and badges</h3>
128
+ <div class='d-flex flex-wrap align-items-center gap-2'>
129
+ <button type='button' class='btn btn-solid theme-primary'>Solid</button>
130
+ <button type='button' class='btn btn-outline theme-primary'>Outline</button>
131
+ <button type='button' class='btn btn-link p-0 align-baseline'>Link</button>
132
+ <span class='badge'>enum</span>
133
+ <span class='badge theme-success'>delivered</span>
134
+ <span class='badge theme-danger'>failed</span>
135
+ </div>
136
+
137
+ <h3 class='h5 mt-4'>Breadcrumb</h3>
138
+ <nav aria-label='Breadcrumb'><ol class='breadcrumb m-0'>
139
+ <li class='breadcrumb-item'><a class='breadcrumb-link' href='#'><i class='bi bi-geo'></i> States</a></li>
140
+ <li class='breadcrumb-item'><span class='breadcrumb-link active'>California</span></li>
141
+ </ol></nav>
142
+
143
+ <h3 class='h5 mt-4'>PIN</h3>
144
+ <p class='fg-2 small'>What <code>pin_field form, :pin_confirmation</code> draws: one real field, six slots by Bootstrap, the code offered by the phone from the text that brought it, and a paste that sends itself.</p>
145
+ <form onsubmit='return false' style='max-width: 22rem'>
146
+ <div class='otp otp-lg' data-controller='otp' data-bs-otp='true'>
147
+ <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'>
148
+ </div>
149
+ </form>
150
+
151
+ <h3 class='h5 mt-4'>Combobox</h3>
152
+ <p class='fg-2 small'>A <code>&lt;select data-controller='combobox'&gt;</code>, dressed by the bundle: a search box, a check per pick where it is <code>multiple</code>, an <code>All</code> row that puts held-back options on the menu, and a figure beside an option from <code>data-count</code>. The select stays what the form submits.</p>
153
+ <div class='d-flex flex-wrap gap-3 align-items-start'>
154
+ <div style='width: 16rem'>
155
+ <label class='form-label' for='team'>Team</label>
156
+ <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'>
157
+ <option value=''>None</option>
158
+ <option value='1'>Blue Crew</option>
159
+ <option value='2' selected>Golden Gate Electric</option>
160
+ <option value='3'>Nashville Plumbing</option>
161
+ </select>
162
+ </div>
163
+ <div style='width: 16rem'>
164
+ <label class='form-label' for='markets'>Markets</label>
165
+ <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'>
166
+ <option value='1' data-count='38,405' selected>San Francisco</option>
167
+ <option value='2' data-count='21' selected>Nashville</option>
168
+ <option value='3' data-count='3'>Austin</option>
169
+ <option value='4' data-count='0' data-hidden>Boise</option>
170
+ </select>
171
+ </div>
172
+ </div>
173
+
174
+ <h3 class='h5 mt-4'>Confirm</h3>
175
+ <p class='fg-2 small'>Any link or form with <code>data-turbo-confirm</code> asks through a Bootstrap dialog the bundle draws on the first ask: the first line is the question, the rest are paragraphs, and the answer wears the words of the button that asked.</p>
176
+ <a class='btn btn-sm btn-solid theme-danger' href='#confirm-demo' data-turbo-method='delete' data-turbo-confirm='Delete California?&#10;&#10;58 counties and 3 markets will be deleted with it.&#10;12 messages will be kept, without a job.&#10;Anything under those goes too.&#10;&#10;This cannot be undone.'>Delete state</a>
177
+
178
+ <h3 class='h5 mt-4'>Tooltip and relative time</h3>
179
+ <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> ·
180
+ <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>
181
+
182
+ <h3 class='h5 mt-4'>Phone</h3>
183
+ <p class='fg-2 small'>The server hands over ten digits and <code>data-controller='phone'</code>; the browser decides the shape, in a cell and in a field.</p>
184
+ <p>A cell: <span data-controller='phone'>4155550000</span></p>
185
+ <div style='max-width: 20rem'>
186
+ <label class='form-label' for='phone'>Phone</label>
187
+ <input class='form-control' type='tel' id='phone' data-controller='phone' data-action='keydown->phone#down input->phone#input' value='4155550000'>
188
+ </div>
189
+
190
+ <h3 class='h5 mt-4'>A masked value</h3>
191
+ <div class='form-control-plaintext d-flex gap-2 align-items-end' data-controller='reveal' data-reveal-plain-value='SEC-0000'>
192
+ <span data-reveal-target='mask'>********</span>
193
+ <button type='button' class='btn btn-link btn-sm p-0' data-action='reveal#show' data-reveal-target='button'>Show</button>
194
+ </div>
195
+
196
+ <h3 class='h5 mt-4'>Toast</h3>
197
+ <div class='toast show' role='status' aria-live='polite' style='position: static'>
198
+ <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>
199
+ </div>
200
+ </section>
201
+
202
+ <section class='design-section' id='chat'>
203
+ <h2>Chat</h2>
204
+ <p class='fg-2'>What the <code>chat_with</code> helper draws: turns in on the left and out on the right, who said each and when, a delivery mark, what a turn cost, an answer being typed, and a field whose suggestion cycles.</p>
205
+ <div class='chat-thread' data-controller='thread'>
206
+ <div class='chat' data-thread-target='messages'>
207
+ <small class='chat-aside chat-aside-in'>Claude <time datetime='2026-09-13T12:00:00-04:00' title='September 13, 2026 12:00'>2 minutes ago</time></small>
208
+ <p class='chat-bubble chat-in'>Data loaded! What can I help with?</p>
209
+ <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>
210
+ <p class='chat-bubble chat-out'>How much did I bill last month?</p>
211
+ <small class='chat-aside chat-aside-in'>Claude <time datetime='2026-09-13T12:01:30-04:00' title='September 13, 2026 12:01'>a minute ago</time> · $0.0123</small>
212
+ <p class='chat-bubble chat-in'><span class='fs-4 d-block'>$12,400</span>Across 31 jobs, up from $9,800 the month before.</p>
213
+ <small class='chat-aside chat-aside-out'>you <strong class='fg-danger'>✕</strong></small>
214
+ <p class='chat-bubble chat-out'>And the month before?</p>
215
+ <p class='chat-bubble chat-in chat-typing' aria-label='Typing'><span></span><span></span><span></span></p>
216
+ </div>
217
+ <form class='d-flex align-items-center' data-controller='require' action='#' onsubmit='return false'>
218
+ <label for='ask' class='visually-hidden'>Ask a question</label>
219
+ <input type='text' name='ask' id='ask' required autocomplete='off' class='form-control rounded-4 flex-grow-1 chat-ask'
220
+ placeholder='How much did I bill last month?' data-thread-target='field' data-controller='placeholder'
221
+ 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?"]'>
222
+ <input type='submit' value='Send' class='btn btn-solid rounded-3 theme-primary ms-3'>
223
+ </form>
224
+ </div>
225
+ </section>
226
+
227
+ <section class='design-section' id='wall'>
228
+ <h2>Wall</h2>
229
+ <p class='fg-2'>A page of the signup flow, as <code>flow</code> draws it around a card: the mark over a cyanotype of the trades a homeowner books, drifting up in columns with the family's marks among them, a title, a line, the card as wide as <code>width:</code> says, and the way out. One attribute draws the wall, <code>data-controller='wall'</code>. Blueprint blues in light, inverted in dark.</p>
230
+ <div class='design-wall'>
231
+ <div class='flow' data-controller='wall'>
232
+ <header><a class='lockup quicksand' href='#wall'><svg aria-hidden='true' style='width: 0.8117em; height: 0.8117em; top: 0.0358em' viewBox='0 0 512 512'><g fill='none' stroke='#3b8ae8' stroke-width='28' stroke-linecap='round' stroke-linejoin='round'><path d='M256 302Q256 256 295.8 233L406.2 169.3Q446 146.3 446 192.3V319.7Q446 365.7 406.2 388.7L295.8 452.4Q256 475.4 256 429.4Z' fill='#1B5288'/><path d='M216.2 59.6Q256 36.6 295.8 59.6L406.2 123.3Q446 146.3 446 192.3V319.7Q446 365.7 406.2 388.7L295.8 452.4Q256 475.4 216.2 452.4L105.8 388.7Q66 365.7 66 319.7V192.3Q66 146.3 105.8 123.3Z'/><path d='M256 256 95.4 163.3'/><path d='M256 256 416.6 163.3'/><path d='M256 256V441.4'/></g></svg><span class='house'>house</span>account</a></header>
233
+ <h1>Welcome to HouseAccount</h1>
234
+ <p>Enter the phone number of your business and we’ll text a verification code</p>
235
+ <div class='flow-card p-3 md:p-4 mx-auto'>
236
+ <form class='p-0' data-controller='require' onsubmit='return false'>
237
+ <fieldset class='d-grid'>
238
+ <label class='form-label' for='wall-phone'>Your mobile number</label>
239
+ <input class='form-control' type='tel' id='wall-phone' required data-controller='phone' data-action='keydown->phone#down input->phone#input'>
240
+ </fieldset>
241
+ <input type='submit' value='Join' class='btn btn-solid theme-primary mt-3'>
242
+ </form>
243
+ </div>
244
+ <footer><a href='#wall'>Sign out</a><span class='fg-2' aria-hidden='true'>·</span><a href='#wall'>Disconnect Jobber</a></footer>
245
+ </div>
246
+ </div>
247
+ </section>
248
+
249
+ <section class='design-section' id='table'>
250
+ <h2>Tables</h2>
251
+ <p class='fg-2'>A recourse table: sortable headings with an icon, a kept row, the row a write just landed on, and cells that stack under 576 pixels.</p>
252
+ <div class='table-responsive' style='container-type: inline-size'>
253
+ <table class='table table-hover sm:table-stacked'>
254
+ <thead><tr>
255
+ <th scope='col'><i class='bi bi-bookmark' role='img' data-controller='tooltip' data-bs-placement='top' data-bs-title='Kept'></i></th>
256
+ <th scope='col'><a href='#'>Name <i class='bi bi-sort-down'></i></a></th>
257
+ <th scope='col'><a href='#'>Phone</a></th>
258
+ <th scope='col'><a href='#'>Status</a></th>
259
+ <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>
260
+ </tr></thead>
261
+ <tbody>
262
+ <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>
263
+ <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>
264
+ <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>
265
+ </tbody>
266
+ </table>
267
+ </div>
268
+ </section>
269
+
270
+ <section class='design-section' id='page'>
271
+ <h2>Pages</h2>
272
+ <p class='fg-2'>What a HouseAccount app answers when it cannot answer, under <code>/page</code>. The <code>houseaccount</code> engine serves them for every status.</p>
273
+ <ul class='list-unstyled d-flex flex-wrap gap-3'>
274
+ <li><a href='page/400'>400 Bad request</a></li>
275
+ <li><a href='page/404'>404 Not found</a></li>
276
+ <li><a href='page/406-unsupported-browser'>406 Unsupported browser</a></li>
277
+ <li><a href='page/422'>422 Unprocessable</a></li>
278
+ <li><a href='page/500'>500 Server error</a></li>
279
+ </ul>
280
+ <p class='fg-2'>And what each app shows while it is down, under <code>/down</code>: its own mark in the middle, the house's at the foot. The engine answers <code>/down.html</code> with the host's own once <code>Houseaccount.app</code> names it, and Heroku's maintenance page points here.</p>
281
+ <ul class='list-unstyled d-flex flex-wrap gap-3'>
282
+ <li><a href='down/fountain'>fountain</a></li>
283
+ <li><a href='down/goldrush'>goldrush</a></li>
284
+ <li><a href='down/houston'>houston</a></li>
285
+ <li><a href='down/autopilot'>autopilot</a></li>
286
+ <li><a href='down/design'>design</a></li>
287
+ </ul>
288
+ </section>
289
+ </main>
290
+
291
+ <footer class='container-fluid px-3 py-4 small fg-2 d-flex flex-wrap gap-3'>
292
+ <span>The <code>houseaccount</code> gem draws all of this in a Rails app.</span>
293
+ <a href='https://github.com/HouseAccountEng/design'>Source</a>
294
+ <a href='https://rubydoc.info/gems/houseaccount'>Reference</a>
295
+ <a href='https://rubygems.org/gems/houseaccount'>RubyGems</a>
296
+ </footer>
297
+ </body>
298
+ </html>
@@ -18,25 +18,26 @@ module HouseAccount
18
18
  tag.meta(name: 'turbo-refresh-scroll', content: 'preserve'),
19
19
  tag.link(rel: 'stylesheet', href: "/v#{VERSION}/css/houseaccount.css"),
20
20
  tag.script('', type: 'module', src: "/v#{VERSION}/js/houseaccount.js"),
21
- *houseaccount_icon_tags,
21
+ houseaccount_icons,
22
22
  ], INDENT
23
23
  end
24
24
 
25
- # An asset on the design site by its path, for a page this engine does not serve.
26
- def houseaccount_url(path) = "#{HouseAccount.url}/#{path}"
27
-
28
- private
29
-
30
- def houseaccount_icon_tags
31
- app = HouseAccount.app
25
+ # The icons a tab, a home screen and a saved app know `app` by — this app unless said:
26
+ # the SVG, its dev variant while developing, the 96px PNG, the apple touch icon and the
27
+ # web manifest, one to a line. Also what `recourses/head` draws for a host of recourse.
28
+ def houseaccount_icons(app = HouseAccount.app)
32
29
  mark = Rails.env.development? ? "#{app}-dev" : app
33
- [
30
+
31
+ safe_join [
34
32
  tag.link(rel: 'icon', href: "/favicon/#{mark}.svg", type: 'image/svg+xml'),
35
33
  tag.link(rel: 'icon', href: "/favicon/#{app}-96.png", type: 'image/png', sizes: '96x96'),
36
34
  tag.link(rel: 'apple-touch-icon', href: "/favicon/#{app}-180.png"),
37
35
  tag.link(rel: 'manifest', href: "/favicon/#{app}.webmanifest"),
38
- ]
36
+ ], INDENT
39
37
  end
38
+
39
+ # An asset on the design site by its path, for a page this engine does not serve.
40
+ def houseaccount_url(path) = "#{HouseAccount.url}/#{path}"
40
41
  end
41
42
  end
42
43
  end
@@ -1,4 +1,4 @@
1
1
  module HouseAccount
2
2
  # Version of the gem, read by the gemspec and by hosts checking compatibility.
3
- VERSION = '0.3.0'
3
+ VERSION = '0.4.0'
4
4
  end
data/public/404.html ADDED
@@ -0,0 +1,118 @@
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
+ <title>The page you were looking for doesn't exist (404 Not found)</title>
7
+ <meta name="color-scheme" content="light dark">
8
+ <link rel="icon" href="/favicon.ico" sizes="32x32">
9
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
10
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
11
+ <link rel="manifest" href="/site.webmanifest">
12
+ <style>
13
+ @font-face {
14
+ font-family: 'Quicksand'; font-style: normal; font-weight: 500; font-display: swap;
15
+ src: url('/font/quicksand-500-vietnamese.woff2') format('woff2');
16
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
17
+ }
18
+ @font-face {
19
+ font-family: 'Quicksand'; font-style: normal; font-weight: 500; font-display: swap;
20
+ src: url('/font/quicksand-500-latin-ext.woff2') format('woff2');
21
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
22
+ }
23
+ @font-face {
24
+ font-family: 'Quicksand'; font-style: normal; font-weight: 500; font-display: swap;
25
+ src: url('/font/quicksand-500-latin.woff2') format('woff2');
26
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
27
+ }
28
+ :root {
29
+ --bg: #fbfcfe;
30
+ --ink: #1B5288;
31
+ --mark: #3b8ae8;
32
+ --muted: #5c6c84;
33
+ }
34
+ @media (prefers-color-scheme: dark) {
35
+ :root {
36
+ --bg: #0f1622;
37
+ --ink: #9cc0ec;
38
+ --muted: #8698af;
39
+ }
40
+ }
41
+ * { box-sizing: border-box; }
42
+ body {
43
+ margin: 0;
44
+ min-height: 100svh;
45
+ display: flex;
46
+ flex-direction: column;
47
+ background: var(--bg);
48
+ font-family: "Quicksand", "Trebuchet MS", sans-serif;
49
+ font-weight: 500;
50
+ letter-spacing: 0.01em;
51
+ }
52
+ main {
53
+ flex: 1;
54
+ display: flex;
55
+ flex-direction: column;
56
+ align-items: center;
57
+ justify-content: center;
58
+ padding: 40px 24px;
59
+ text-align: center;
60
+ }
61
+ .lockup {
62
+ white-space: nowrap;
63
+ max-width: 100%;
64
+ color: var(--ink);
65
+ line-height: 1;
66
+ margin: 0;
67
+ }
68
+ .lockup svg {
69
+ position: relative;
70
+ vertical-align: baseline;
71
+ }
72
+ main .lockup {
73
+ font-size: clamp(1.5rem, 5.5vw, 2.5rem);
74
+ font-weight: 500;
75
+ /* The longest heading is wider than a phone, so let it wrap rather than
76
+ overflow; the mark stays attached to the status code on the first line. */
77
+ white-space: normal;
78
+ text-wrap: balance;
79
+ line-height: 1.25;
80
+ }
81
+ main .lockup svg {
82
+ width: 1.0073em; height: 1.0073em;
83
+ top: 0.1337em;
84
+ margin-right: 0.1327em;
85
+ }
86
+ main p {
87
+ margin: 22px 0 0;
88
+ color: var(--muted);
89
+ font-size: 1rem;
90
+ line-height: 1.6;
91
+ }
92
+ footer {
93
+ position: sticky;
94
+ bottom: 0;
95
+ background: var(--bg);
96
+ display: flex;
97
+ justify-content: center;
98
+ padding: 20px 0 26px;
99
+ }
100
+ footer .lockup { font-size: 20px; }
101
+ footer .lockup svg {
102
+ width: 16.23px; height: 16.23px;
103
+ top: 0.72px;
104
+ margin-right: 4.2px;
105
+ }
106
+ footer .house { color: var(--mark); }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <main>
111
+ <h1 class="lockup"><svg viewBox="0 0 512 512" aria-hidden="true"><g fill="none" stroke="#3b8ae8" stroke-width="28" stroke-linecap="round"><path d="M251.4 235.6 C254.8 239.8 256.5 246.9 254 254.2 C251.6 261.5 245 268.8 235.4 272.3 C225.8 275.7 213.3 275.1 202.2 268.8 C191 262.6 181.5 250.6 178.1 235.6 C174.6 220.6 177.5 202.7 187.6 187.7 C197.6 172.8 215 161 235.4 157.6 C255.8 154.1 279.1 159.2 297.9 173.1 C316.6 187 330.6 209.8 334.1 235.6 C337.5 261.4 330.2 290.1 312.5 312.7 C294.7 335.3 266.6 351.5 235.4 354.9 C204.2 358.4 170.2 348.8 143.7 327.3 C117.3 305.7 98.8 272.2 95.4 235.6 C92 198.9 103.7 159.6 129.1 129.3 C154.5 99 193.3 78.4 235.4 74.9 C277.5 71.5 322.2 85.5 356.3 114.7 C390.4 143.9 413.3 188.1 416.7 235.6 C420.2 283.1 404 333.2 370.9 371.1 C337.9 409 288.3 434.1 235.4 437.6"/></g></svg>404 Not found</h1>
112
+ <p>You may have mistyped the address or the page may have moved.</p>
113
+ </main>
114
+ <footer>
115
+ <p class="lockup"><svg viewBox="0 0 512 512" aria-hidden="true"><g fill="none" stroke="#3b8ae8" stroke-width="28" stroke-linecap="round" stroke-linejoin="round"><path d="M256.0 302.0 Q256.0 256.0 295.8 233.0 L406.2 169.3 Q446.0 146.3 446.0 192.3 L446.0 319.7 Q446.0 365.7 406.2 388.7 L295.8 452.4 Q256.0 475.4 256.0 429.4 Z" fill="#1B5288"/><path d="M216.2 59.6 Q256.0 36.6 295.8 59.6 L406.2 123.3 Q446.0 146.3 446.0 192.3 L446.0 319.7 Q446.0 365.7 406.2 388.7 L295.8 452.4 Q256.0 475.4 216.2 452.4 L105.8 388.7 Q66.0 365.7 66.0 319.7 L66.0 192.3 Q66.0 146.3 105.8 123.3 Z"/><path d="M256.0 256.0 L95.4 163.3"/><path d="M256.0 256.0 L416.6 163.3"/><path d="M256.0 256.0 L256.0 441.4"/></g></svg><span class="house">house</span>account</p>
116
+ </footer>
117
+ </body>
118
+ </html>
data/public/index.html CHANGED
@@ -66,8 +66,8 @@
66
66
  </div>
67
67
  <div class='col-12'>
68
68
  <div class='card p-3'>
69
- <strong>Pinned to a version</strong>
70
- <span class='design-url'>https://design.houseaccount.com/v0.2.0/css/houseaccount.css · /v0.2.0/js/houseaccount.js · /v0.2.0/theme/nord.css</span>
69
+ <strong>Pinned to a version — the latest is 0.4.0</strong>
70
+ <span class='design-url'>https://design.houseaccount.com/v0.4.0/css/houseaccount.css · /v0.4.0/js/houseaccount.js · /v0.4.0/theme/nord.css</span>
71
71
  <span class='small fg-2 mt-1'>Every release stays at its own prefix forever, whole and cached for a year, for a page that moves on its own terms. The unversioned paths follow the latest.</span>
72
72
  </div>
73
73
  </div>