houseaccount 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +163 -1
- data/MIT-LICENSE +22 -0
- data/README.md +230 -20
- data/art/autopilot.svg +9 -0
- data/art/design.svg +8 -0
- data/art/down.html.erb +95 -0
- data/art/fountain.svg +9 -0
- data/art/goldrush.svg +7 -0
- data/art/houseaccount.svg +9 -0
- data/art/houston.svg +8 -0
- data/art/icons.txt +184 -0
- data/config/locales/houseaccount.en.yml +12 -0
- data/lib/houseaccount/down.rb +19 -0
- data/lib/houseaccount/engine.rb +53 -0
- data/lib/houseaccount/form_builder/pins.rb +30 -0
- data/lib/houseaccount/form_builder.rb +84 -0
- data/lib/houseaccount/helpers/chats/asides.rb +49 -0
- data/lib/houseaccount/helpers/chats/asks.rb +40 -0
- data/lib/houseaccount/helpers/chats.rb +42 -0
- data/lib/houseaccount/helpers/dialogs.rb +47 -0
- data/lib/houseaccount/helpers/flows.rb +45 -0
- data/lib/houseaccount/helpers/heads.rb +42 -0
- data/lib/houseaccount/helpers/logos.rb +43 -0
- data/lib/houseaccount/helpers/notices.rb +55 -0
- data/lib/houseaccount/helpers.rb +13 -0
- data/lib/houseaccount/marks.rb +14 -0
- data/lib/houseaccount/message.rb +11 -0
- data/lib/houseaccount/version.rb +2 -2
- data/lib/houseaccount.rb +35 -2
- data/public/_headers +28 -0
- data/public/css/houseaccount.css +1 -0
- data/public/down/autopilot.html +87 -0
- data/public/down/design.html +87 -0
- data/public/down/fountain.html +87 -0
- data/public/down/goldrush.html +87 -0
- data/public/down/houseaccount.html +84 -0
- data/public/down/houston.html +87 -0
- data/public/favicon/autopilot-180.png +0 -0
- data/public/favicon/autopilot-192.png +0 -0
- data/public/favicon/autopilot-512.png +0 -0
- data/public/favicon/autopilot-96.png +0 -0
- data/public/favicon/autopilot-dev.svg +9 -0
- data/public/favicon/autopilot.ico +0 -0
- data/public/favicon/autopilot.svg +9 -0
- data/public/favicon/autopilot.webmanifest +12 -0
- data/public/favicon/design-180.png +0 -0
- data/public/favicon/design-192.png +0 -0
- data/public/favicon/design-512.png +0 -0
- data/public/favicon/design-96.png +0 -0
- data/public/favicon/design-dev.svg +8 -0
- data/public/favicon/design.ico +0 -0
- data/public/favicon/design.svg +8 -0
- data/public/favicon/design.webmanifest +12 -0
- data/public/favicon/fountain-180.png +0 -0
- data/public/favicon/fountain-192.png +0 -0
- data/public/favicon/fountain-512.png +0 -0
- data/public/favicon/fountain-96.png +0 -0
- data/public/favicon/fountain-dev.svg +9 -0
- data/public/favicon/fountain.ico +0 -0
- data/public/favicon/fountain.svg +9 -0
- data/public/favicon/fountain.webmanifest +12 -0
- data/public/favicon/goldrush-180.png +0 -0
- data/public/favicon/goldrush-192.png +0 -0
- data/public/favicon/goldrush-512.png +0 -0
- data/public/favicon/goldrush-96.png +0 -0
- data/public/favicon/goldrush-dev.svg +7 -0
- data/public/favicon/goldrush.ico +0 -0
- data/public/favicon/goldrush.svg +7 -0
- data/public/favicon/goldrush.webmanifest +12 -0
- data/public/favicon/houseaccount-180.png +0 -0
- data/public/favicon/houseaccount-192.png +0 -0
- data/public/favicon/houseaccount-512.png +0 -0
- data/public/favicon/houseaccount-96.png +0 -0
- data/public/favicon/houseaccount-dev.svg +9 -0
- data/public/favicon/houseaccount.ico +0 -0
- data/public/favicon/houseaccount.svg +9 -0
- data/public/favicon/houseaccount.webmanifest +12 -0
- data/public/favicon/houston-180.png +0 -0
- data/public/favicon/houston-192.png +0 -0
- data/public/favicon/houston-512.png +0 -0
- data/public/favicon/houston-96.png +0 -0
- data/public/favicon/houston-dev.svg +8 -0
- data/public/favicon/houston.ico +0 -0
- data/public/favicon/houston.svg +8 -0
- data/public/favicon/houston.webmanifest +12 -0
- data/public/favicon.ico +0 -0
- data/public/font/bootstrap-icons-6LILWPE2.woff2 +0 -0
- data/public/font/quicksand-500-latin-QHYSRFVK.woff2 +0 -0
- data/public/font/quicksand-500-latin-ext-QZ4PXVHB.woff2 +0 -0
- data/public/font/quicksand-500-latin-ext.woff2 +0 -0
- data/public/font/quicksand-500-latin.woff2 +0 -0
- data/public/font/quicksand-500-vietnamese-RRA5CUQB.woff2 +0 -0
- data/public/font/quicksand-500-vietnamese.woff2 +0 -0
- data/public/index.html +298 -0
- data/public/js/houseaccount.js +144 -0
- data/public/logo/autopilot.png +0 -0
- data/public/logo/autopilot.svg +9 -0
- data/public/logo/design.png +0 -0
- data/public/logo/design.svg +8 -0
- data/public/logo/fountain.png +0 -0
- data/public/logo/fountain.svg +9 -0
- data/public/logo/goldrush.png +0 -0
- data/public/logo/goldrush.svg +7 -0
- data/public/logo/houseaccount.png +0 -0
- data/public/logo/houseaccount.svg +9 -0
- data/public/logo/houston.png +0 -0
- data/public/logo/houston.svg +8 -0
- data/public/page/400.html +118 -0
- data/public/page/404.html +118 -0
- data/public/page/406-unsupported-browser.html +118 -0
- data/public/page/422.html +118 -0
- data/public/page/500.html +118 -0
- data/public/site.webmanifest +12 -0
- data/public/theme/bootstrap.css +11 -0
- data/public/theme/dawn.css +189 -0
- data/public/theme/dracula.css +189 -0
- data/public/theme/gruvbox.css +189 -0
- data/public/theme/monokai.css +190 -0
- data/public/theme/nord.css +189 -0
- data/public/theme/one_dark.css +189 -0
- data/public/theme/solarized.css +188 -0
- data/public/theme/tokyo_night.css +189 -0
- data/public/v0.3.0/css/houseaccount.css +1 -0
- data/public/v0.3.0/font/bootstrap-icons-6LILWPE2.woff2 +0 -0
- data/public/v0.3.0/font/quicksand-500-latin-QHYSRFVK.woff2 +0 -0
- data/public/v0.3.0/font/quicksand-500-latin-ext-QZ4PXVHB.woff2 +0 -0
- data/public/v0.3.0/font/quicksand-500-latin-ext.woff2 +0 -0
- data/public/v0.3.0/font/quicksand-500-latin.woff2 +0 -0
- data/public/v0.3.0/font/quicksand-500-vietnamese-RRA5CUQB.woff2 +0 -0
- data/public/v0.3.0/font/quicksand-500-vietnamese.woff2 +0 -0
- data/public/v0.3.0/js/houseaccount.js +144 -0
- data/public/v0.3.0/theme/bootstrap.css +11 -0
- data/public/v0.3.0/theme/dawn.css +189 -0
- data/public/v0.3.0/theme/dracula.css +189 -0
- data/public/v0.3.0/theme/gruvbox.css +189 -0
- data/public/v0.3.0/theme/monokai.css +190 -0
- data/public/v0.3.0/theme/nord.css +189 -0
- data/public/v0.3.0/theme/one_dark.css +189 -0
- data/public/v0.3.0/theme/solarized.css +188 -0
- data/public/v0.3.0/theme/tokyo_night.css +189 -0
- metadata +190 -12
- data/LICENSE.txt +0 -21
data/art/icons.txt
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Every Bootstrap Icon a HouseAccount page may draw: the ones unicon names for its concepts,
|
|
2
|
+
# and the few written out by name in recourse and here. The icon font and its stylesheet
|
|
3
|
+
# are cut down to these by rake icons; a name missing here is a blank box on a page.
|
|
4
|
+
airplane
|
|
5
|
+
alarm
|
|
6
|
+
archive
|
|
7
|
+
arrow-clockwise
|
|
8
|
+
arrow-counterclockwise
|
|
9
|
+
arrow-down
|
|
10
|
+
arrow-left
|
|
11
|
+
arrow-right
|
|
12
|
+
arrow-up
|
|
13
|
+
arrows-angle-contract
|
|
14
|
+
arrows-angle-expand
|
|
15
|
+
award
|
|
16
|
+
bag
|
|
17
|
+
bank
|
|
18
|
+
bar-chart
|
|
19
|
+
battery
|
|
20
|
+
bell
|
|
21
|
+
bicycle
|
|
22
|
+
book
|
|
23
|
+
bookmark
|
|
24
|
+
bookmark-fill
|
|
25
|
+
box
|
|
26
|
+
box-arrow-in-right
|
|
27
|
+
box-arrow-right
|
|
28
|
+
briefcase
|
|
29
|
+
brush
|
|
30
|
+
building
|
|
31
|
+
bus-front
|
|
32
|
+
c-circle
|
|
33
|
+
calendar
|
|
34
|
+
calendar-check
|
|
35
|
+
camera
|
|
36
|
+
camera-video
|
|
37
|
+
car-front
|
|
38
|
+
card-checklist
|
|
39
|
+
cart
|
|
40
|
+
cash
|
|
41
|
+
chat
|
|
42
|
+
check
|
|
43
|
+
check-circle
|
|
44
|
+
check-circle-fill
|
|
45
|
+
check2-square
|
|
46
|
+
chevron-down
|
|
47
|
+
chevron-left
|
|
48
|
+
chevron-right
|
|
49
|
+
chevron-up
|
|
50
|
+
circle
|
|
51
|
+
clipboard
|
|
52
|
+
clock
|
|
53
|
+
cloud
|
|
54
|
+
cloud-rain
|
|
55
|
+
collection-play
|
|
56
|
+
compass
|
|
57
|
+
copy
|
|
58
|
+
credit-card
|
|
59
|
+
dash
|
|
60
|
+
display
|
|
61
|
+
download
|
|
62
|
+
droplet
|
|
63
|
+
emoji-smile
|
|
64
|
+
envelope
|
|
65
|
+
exclamation-triangle
|
|
66
|
+
eye
|
|
67
|
+
eye-slash
|
|
68
|
+
file-earmark
|
|
69
|
+
file-earmark-text
|
|
70
|
+
fingerprint
|
|
71
|
+
fire
|
|
72
|
+
flag
|
|
73
|
+
folder
|
|
74
|
+
funnel
|
|
75
|
+
gear
|
|
76
|
+
geo
|
|
77
|
+
geo-alt
|
|
78
|
+
gift
|
|
79
|
+
globe
|
|
80
|
+
graph-up
|
|
81
|
+
grid
|
|
82
|
+
grip-vertical
|
|
83
|
+
hammer
|
|
84
|
+
hand-thumbs-down
|
|
85
|
+
hand-thumbs-up
|
|
86
|
+
hdd-rack
|
|
87
|
+
headphones
|
|
88
|
+
heart
|
|
89
|
+
hospital
|
|
90
|
+
hourglass
|
|
91
|
+
house
|
|
92
|
+
image
|
|
93
|
+
inbox
|
|
94
|
+
infinity
|
|
95
|
+
info-circle
|
|
96
|
+
key
|
|
97
|
+
keyboard
|
|
98
|
+
laptop
|
|
99
|
+
layout-sidebar
|
|
100
|
+
lightbulb
|
|
101
|
+
lightning
|
|
102
|
+
link
|
|
103
|
+
list
|
|
104
|
+
list-ul
|
|
105
|
+
lock
|
|
106
|
+
magic
|
|
107
|
+
map
|
|
108
|
+
megaphone
|
|
109
|
+
mic
|
|
110
|
+
moon
|
|
111
|
+
moon-fill
|
|
112
|
+
music-note
|
|
113
|
+
palette
|
|
114
|
+
paperclip
|
|
115
|
+
pause
|
|
116
|
+
pen
|
|
117
|
+
pencil
|
|
118
|
+
people
|
|
119
|
+
percent
|
|
120
|
+
person
|
|
121
|
+
person-circle
|
|
122
|
+
person-dash
|
|
123
|
+
person-plus
|
|
124
|
+
person-rolodex
|
|
125
|
+
person-vcard
|
|
126
|
+
phone
|
|
127
|
+
pie-chart
|
|
128
|
+
play
|
|
129
|
+
plus
|
|
130
|
+
power
|
|
131
|
+
printer
|
|
132
|
+
puzzle
|
|
133
|
+
qr-code
|
|
134
|
+
question-circle
|
|
135
|
+
receipt
|
|
136
|
+
repeat
|
|
137
|
+
reply
|
|
138
|
+
rulers
|
|
139
|
+
scissors
|
|
140
|
+
search
|
|
141
|
+
send
|
|
142
|
+
share
|
|
143
|
+
shield
|
|
144
|
+
shop
|
|
145
|
+
shuffle
|
|
146
|
+
signpost-split
|
|
147
|
+
skip-forward
|
|
148
|
+
sliders
|
|
149
|
+
snow
|
|
150
|
+
sort-down
|
|
151
|
+
soundwave
|
|
152
|
+
speedometer
|
|
153
|
+
square
|
|
154
|
+
star
|
|
155
|
+
sticky
|
|
156
|
+
stop
|
|
157
|
+
stopwatch
|
|
158
|
+
sun
|
|
159
|
+
sun-fill
|
|
160
|
+
table
|
|
161
|
+
tag
|
|
162
|
+
telephone
|
|
163
|
+
terminal
|
|
164
|
+
three-dots
|
|
165
|
+
ticket
|
|
166
|
+
tools
|
|
167
|
+
train-front
|
|
168
|
+
trash
|
|
169
|
+
tree
|
|
170
|
+
trophy
|
|
171
|
+
truck
|
|
172
|
+
unlock
|
|
173
|
+
upc
|
|
174
|
+
upload
|
|
175
|
+
volume-up
|
|
176
|
+
wallet
|
|
177
|
+
wifi
|
|
178
|
+
window
|
|
179
|
+
wrench
|
|
180
|
+
x
|
|
181
|
+
x-circle
|
|
182
|
+
x-lg
|
|
183
|
+
zoom-in
|
|
184
|
+
zoom-out
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'rack/files'
|
|
2
|
+
|
|
3
|
+
module HouseAccount
|
|
4
|
+
# Answers `/down.html` with the page the host shows while it is down: its own where it is
|
|
5
|
+
# one of the family, the house's otherwise.
|
|
6
|
+
class Down
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
@files = Rack::Files.new Engine.root.join('public/down').to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The page where the path asks for it, and the host otherwise.
|
|
13
|
+
def call(env)
|
|
14
|
+
return @app.call env unless env['PATH_INFO'] == '/down.html'
|
|
15
|
+
|
|
16
|
+
@files.call env.merge('PATH_INFO' => "/#{HouseAccount.app}.html")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rails/engine alone raises NoMethodError: railtie.rb calls delegate_missing_to too early.
|
|
2
|
+
require 'rails'
|
|
3
|
+
|
|
4
|
+
module HouseAccount
|
|
5
|
+
# Hooks the gem into a host app: serves its files, its error pages and its helpers.
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
# The folders the design site publishes, served from this gem at the same paths so a
|
|
8
|
+
# page on an app and a page on the site link one file — this version's copy under its
|
|
9
|
+
# own prefix among them. Each keeps its slash: `Rack::Static` matches on `start_with?`,
|
|
10
|
+
# and `/page` would swallow `/pages` itself.
|
|
11
|
+
PREFIXES = %W[/css/ /js/ /theme/ /font/ /logo/ /favicon/ /page/ /v#{VERSION}/].freeze
|
|
12
|
+
|
|
13
|
+
# An unversioned file is asked for again on every full load and answered `304 Not
|
|
14
|
+
# Modified` while it stands, since its URL never changes between versions. A font, whose
|
|
15
|
+
# name carries a hash of its bytes, and anything under this version's prefix never change
|
|
16
|
+
# at all, so a browser may keep them for good.
|
|
17
|
+
HEADERS = [
|
|
18
|
+
[:all, { 'cache-control' => 'no-cache' }],
|
|
19
|
+
['/font/', { 'cache-control' => 'public, max-age=31536000, immutable' }],
|
|
20
|
+
['/v', { 'cache-control' => 'public, max-age=31536000, immutable' }],
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# What a file is kept for while the gem is being worked on: not at all, since the
|
|
24
|
+
# version's copy is rewritten under the same name until it is released, and a browser
|
|
25
|
+
# told to keep it for a year would show yesterday's stylesheet on today's page.
|
|
26
|
+
DEVELOPMENT_HEADERS = HEADERS.first 1
|
|
27
|
+
|
|
28
|
+
# Before `Rails::Rack::Logger`, so fetching a stylesheet writes no `Started GET` line, and
|
|
29
|
+
# cascading, so a host route under one of these prefixes still answers.
|
|
30
|
+
initializer 'houseaccount.assets' do |app|
|
|
31
|
+
headers = Rails.env.development? ? DEVELOPMENT_HEADERS : HEADERS
|
|
32
|
+
app.middleware.insert_before Rails::Rack::Logger, Rack::Static,
|
|
33
|
+
urls: PREFIXES, root: Engine.root.join('public').to_s,
|
|
34
|
+
header_rules: headers, cascade: true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The branded error pages, read by status from `public/page` the way Rails reads a host's
|
|
38
|
+
# own `public/`, unless the host has already named an app of its own.
|
|
39
|
+
initializer 'houseaccount.exceptions' do |app|
|
|
40
|
+
app.config.exceptions_app ||=
|
|
41
|
+
ActionDispatch::PublicExceptions.new Engine.root.join('public/page').to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The page a host shows while it is down, at the path Heroku and a reader expect.
|
|
45
|
+
initializer 'houseaccount.down' do |app|
|
|
46
|
+
app.middleware.insert_before Rails::Rack::Logger, Down
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
initializer 'houseaccount.helpers' do
|
|
50
|
+
ActiveSupport.on_load(:action_view) { include HouseAccount::Helpers }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'action_view'
|
|
2
|
+
|
|
3
|
+
module HouseAccount
|
|
4
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
5
|
+
# The field Rails has none of: the six slots a code is typed into.
|
|
6
|
+
module Pins
|
|
7
|
+
# What the one real PIN field carries: digits only, six of them, offered by the phone from
|
|
8
|
+
# the text that brought them, and drawn as slots by Bootstrap and the `otp` controller.
|
|
9
|
+
PIN = {
|
|
10
|
+
class: 'otp-input', inputmode: :numeric, pattern: '[0-9]{6}', minlength: 6, maxlength: 6,
|
|
11
|
+
autocomplete: 'one-time-code',
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# `method` as Bootstrap's OTP input: one real field rendered as six slots, the slots
|
|
15
|
+
# reddened where the record refused the code. Every option is the field's. The field is
|
|
16
|
+
# drawn by the view rather than this builder on purpose, so it is never dressed as a
|
|
17
|
+
# `form-control`: a refused one would wear a red box of its own around slots that say so.
|
|
18
|
+
def pin_field(method, **options)
|
|
19
|
+
invalid = @object.respond_to?(:errors) && @object.errors.include?(method)
|
|
20
|
+
classes = @template.class_names 'otp otp-lg', 'is-invalid': invalid
|
|
21
|
+
|
|
22
|
+
field = with_classes PIN.merge(options), PIN[:class]
|
|
23
|
+
|
|
24
|
+
@template.tag.div class: classes, data: { controller: 'otp', bs_otp: true } do
|
|
25
|
+
@template.text_field @object_name, method, **field, object: @object
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require 'action_view'
|
|
2
|
+
require_relative 'form_builder/pins'
|
|
3
|
+
|
|
4
|
+
module HouseAccount
|
|
5
|
+
# The house's form: what a label, a field and a button of one wear, and the field Rails has
|
|
6
|
+
# none of. Given to one form as `builder:`, or to every form of a controller through
|
|
7
|
+
# `default_form_builder`. What a view passes is kept: its classes join the house's, its
|
|
8
|
+
# `data` is merged key by key with the house's, and any other option of its overrides.
|
|
9
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
10
|
+
include Pins
|
|
11
|
+
|
|
12
|
+
# What a field wears: Bootstrap's control, at the size a thumb finds.
|
|
13
|
+
CONTROL = 'form-control form-control-lg'
|
|
14
|
+
|
|
15
|
+
# What a button wears: the large pill in the primary color, solid or outlined.
|
|
16
|
+
PILL = 'btn btn-lg rounded-5 theme-primary'
|
|
17
|
+
|
|
18
|
+
# What clears a button of the field or the button above it.
|
|
19
|
+
SPACE = 'mt-3 md:mt-4'
|
|
20
|
+
|
|
21
|
+
# What a phone field carries: the bundle's `phone` controller, which shapes the number as
|
|
22
|
+
# it is typed, and the keyboard and autofill a phone offers for one.
|
|
23
|
+
PHONE = {
|
|
24
|
+
autocomplete: 'tel',
|
|
25
|
+
data: { controller: 'phone', action: 'keydown->phone#down input->phone#input' },
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# The fieldset a form's controls sit in: a grid one column wide, so each takes the whole
|
|
29
|
+
# row, with `legend` over them as a `form-label` where one is given. Every option is the
|
|
30
|
+
# fieldset's.
|
|
31
|
+
def fieldset(legend = nil, **options, &)
|
|
32
|
+
heading = (@template.tag.legend legend, class: 'form-label' if legend)
|
|
33
|
+
inside = @template.safe_join [heading, @template.capture(&)].compact
|
|
34
|
+
|
|
35
|
+
@template.tag.fieldset inside, **with_classes(options, 'd-grid')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A label is Bootstrap's `form-label`, whatever else it is given.
|
|
39
|
+
def label(method, text = nil, options = {}, &)
|
|
40
|
+
return label(method, nil, text, &) if text.is_a? Hash
|
|
41
|
+
|
|
42
|
+
super(method, text, with_classes(options, 'form-label'), &)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A text field is a large control.
|
|
46
|
+
def text_field(method, options = {}) = super(method, with_classes(options, CONTROL))
|
|
47
|
+
|
|
48
|
+
# A phone field is a large control shaped by the `phone` controller as it is typed.
|
|
49
|
+
def phone_field(method, options = {})
|
|
50
|
+
super(method, PHONE.deep_merge(with_classes(options, CONTROL)))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A submit is the house's button, solid.
|
|
54
|
+
def submit(value = nil, options = {})
|
|
55
|
+
super(value, with_classes(options, "#{PILL} btn-solid #{SPACE}"))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A button is the house's too: solid where it submits, which is what one does unless told
|
|
59
|
+
# `type: :button`, and outlined where it does not. A disabled one given a `title` says why
|
|
60
|
+
# on hover — `Available soon` — from a wrapper around it, since a disabled button hears no
|
|
61
|
+
# mouse; the wrapper then takes the button's place in the column.
|
|
62
|
+
def button(value = nil, options = {}, &)
|
|
63
|
+
return button(nil, value, &) if value.is_a? Hash
|
|
64
|
+
|
|
65
|
+
wrapped = options[:disabled] && options[:title]
|
|
66
|
+
fill = options[:type].to_s == 'button' ? 'btn-outline' : 'btn-solid'
|
|
67
|
+
classes = @template.class_names PILL, fill, (SPACE unless wrapped)
|
|
68
|
+
button = super(value, with_classes(options.except(:title), classes), &)
|
|
69
|
+
|
|
70
|
+
wrapped ? tooltipped(button, options[:title]) : button
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def tooltipped(button, title)
|
|
76
|
+
@template.tag.span button, class: "d-grid #{SPACE}",
|
|
77
|
+
data: { controller: 'tooltip', bs_title: title }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def with_classes(options, classes)
|
|
81
|
+
options.merge class: @template.class_names(classes, options[:class])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module HouseAccount
|
|
2
|
+
module Helpers
|
|
3
|
+
module Chats
|
|
4
|
+
# Who sent a message, when, whether it arrived and what it cost, small above its bubble.
|
|
5
|
+
module Asides
|
|
6
|
+
# Which bubble: the other party's comes in on the left, the reader's goes out on the right.
|
|
7
|
+
SIDES = { left: 'in', right: 'out' }.freeze
|
|
8
|
+
|
|
9
|
+
# How a delivery reads, and in which of Bootstrap's tones: a mark for arrived, a mark
|
|
10
|
+
# for sent and not yet, a cross for either way it failed.
|
|
11
|
+
DELIVERIES = {
|
|
12
|
+
delivered: %w[✓ success], sent: %w[✓ secondary],
|
|
13
|
+
failed: %w[✕ danger], undelivered: %w[✕ danger],
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def houseaccount_chat_aside(message)
|
|
19
|
+
parts = [
|
|
20
|
+
message.sender, houseaccount_chat_time(message.at),
|
|
21
|
+
houseaccount_chat_delivery(message.delivery), houseaccount_chat_cost(message.cost),
|
|
22
|
+
]
|
|
23
|
+
return if parts.compact.empty?
|
|
24
|
+
|
|
25
|
+
side = SIDES.fetch message.side
|
|
26
|
+
tag.small safe_join(parts.compact, ' '), class: "chat-aside chat-aside-#{side}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def houseaccount_chat_time(at)
|
|
30
|
+
return unless at
|
|
31
|
+
|
|
32
|
+
tag.time t('houseaccount.ago', time: time_ago_in_words(at)),
|
|
33
|
+
datetime: at.xmlschema, title: l(at, format: :long)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def houseaccount_chat_delivery(delivery)
|
|
37
|
+
return unless delivery
|
|
38
|
+
|
|
39
|
+
mark, tone = DELIVERIES.fetch delivery.to_sym
|
|
40
|
+
tag.strong mark, class: "fg-#{tone}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def houseaccount_chat_cost(cost)
|
|
44
|
+
"· #{number_to_currency cost, precision: 4}" if cost
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module HouseAccount
|
|
2
|
+
module Helpers
|
|
3
|
+
module Chats
|
|
4
|
+
# The field under a thread that sends the next question.
|
|
5
|
+
module Asks
|
|
6
|
+
# A solid button in the primary, rounded like the field beside it.
|
|
7
|
+
SEND_CLASSES = 'btn btn-solid rounded-3 theme-primary ms-3'
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# `replace`, so the redirect back to the page the form is on is a page refresh to Turbo
|
|
12
|
+
# — morphed in place, scroll kept — rather than a visit that lands at the top.
|
|
13
|
+
def houseaccount_chat_ask(url, hints)
|
|
14
|
+
data = { controller: 'require', turbo_action: 'replace' }
|
|
15
|
+
form_with url:, class: 'd-flex align-items-center', data: do
|
|
16
|
+
safe_join [
|
|
17
|
+
tag.label(t('houseaccount.ask'), for: 'ask', class: 'visually-hidden'),
|
|
18
|
+
text_field_tag(:ask, nil, **houseaccount_chat_ask_options(hints)),
|
|
19
|
+
submit_tag(t('houseaccount.send'), class: SEND_CLASSES),
|
|
20
|
+
], "\n"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def houseaccount_chat_ask_options(hints)
|
|
25
|
+
{
|
|
26
|
+
id: 'ask', required: true, autofocus: true, autocomplete: 'off',
|
|
27
|
+
class: 'form-control rounded-4 flex-grow-1 chat-ask', placeholder: hints.first,
|
|
28
|
+
data: { thread_target: 'field', **houseaccount_chat_placeholder_data(hints) },
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def houseaccount_chat_placeholder_data(hints)
|
|
33
|
+
return {} if hints.size < 2
|
|
34
|
+
|
|
35
|
+
{ controller: 'placeholder', placeholder_questions_value: hints.to_json }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative 'chats/asides'
|
|
2
|
+
require_relative 'chats/asks'
|
|
3
|
+
|
|
4
|
+
module HouseAccount
|
|
5
|
+
module Helpers
|
|
6
|
+
# A conversation drawn as the bubbles every HouseAccount chat wears.
|
|
7
|
+
module Chats
|
|
8
|
+
include Asides, Asks
|
|
9
|
+
|
|
10
|
+
# `messages` as a thread that opens at its newest and, where `url` is given, a field
|
|
11
|
+
# under it that posts the next message there, suggesting `hints` one at a time.
|
|
12
|
+
def chat_with(messages:, url: nil, hints: [])
|
|
13
|
+
parts = [houseaccount_chat_messages(messages)]
|
|
14
|
+
parts.push houseaccount_chat_ask(url, hints) if url
|
|
15
|
+
|
|
16
|
+
tag.div safe_join(parts, "\n"), class: 'chat-thread', data: { controller: 'thread' }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def houseaccount_chat_messages(messages)
|
|
22
|
+
tag.div class: 'chat', data: { thread_target: 'messages' } do
|
|
23
|
+
safe_join messages.map { |message| houseaccount_chat_message message }, "\n"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def houseaccount_chat_message(message)
|
|
28
|
+
parts = [houseaccount_chat_aside(message), houseaccount_chat_bubble(message)]
|
|
29
|
+
|
|
30
|
+
safe_join parts.compact, "\n"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def houseaccount_chat_bubble(message)
|
|
34
|
+
side = Asides::SIDES.fetch message.side
|
|
35
|
+
return tag.p message.text, class: "chat-bubble chat-#{side}" if message.text
|
|
36
|
+
|
|
37
|
+
tag.p safe_join(Array.new(3) { tag.span }), class: "chat-bubble chat-#{side} chat-typing",
|
|
38
|
+
aria: { label: t('houseaccount.typing') }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module HouseAccount
|
|
2
|
+
module Helpers
|
|
3
|
+
# A dialog a page opens from a link, with none of Bootstrap's markup written by hand.
|
|
4
|
+
module Dialogs
|
|
5
|
+
# A link reading `text` and the dialog it opens: headed `title` with an X to close it,
|
|
6
|
+
# holding the block, and closed as well by a footer button reading `okay` — `Okay`
|
|
7
|
+
# unless told otherwise, or none at all for `false`. The two share an id made from the
|
|
8
|
+
# link's own words, so a page needs to invent none; Bootstrap's own `data-bs-toggle`
|
|
9
|
+
# does the opening, and the browser's element brings the backdrop, the focus trap and
|
|
10
|
+
# Esc. Any other option is an attribute of the link.
|
|
11
|
+
def dialog(text, title:, okay: t('houseaccount.okay'), **options, &)
|
|
12
|
+
id = "dialog-#{text.parameterize}"
|
|
13
|
+
data = { **options.fetch(:data, {}), bs_toggle: 'dialog', bs_target: "##{id}" }
|
|
14
|
+
parts = [houseaccount_dialog_header(id, title), tag.div(capture(&), class: 'dialog-body')]
|
|
15
|
+
parts.push houseaccount_dialog_footer(okay) if okay
|
|
16
|
+
|
|
17
|
+
safe_join [
|
|
18
|
+
link_to(text, "##{id}", **options, data:),
|
|
19
|
+
tag.dialog(safe_join(parts), class: 'dialog dialog-slide-down', id:,
|
|
20
|
+
aria: { labelledby: "#{id}-title" }),
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def houseaccount_dialog_header(id, title)
|
|
27
|
+
tag.div class: 'dialog-header' do
|
|
28
|
+
heading = tag.h2 title, class: 'dialog-title', id: "#{id}-title"
|
|
29
|
+
|
|
30
|
+
safe_join [heading, houseaccount_dialog_close]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def houseaccount_dialog_close
|
|
35
|
+
tag.button type: 'button', class: 'btn-close ms-auto', data: { bs_dismiss: 'dialog' },
|
|
36
|
+
aria: { label: t('houseaccount.close') }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def houseaccount_dialog_footer(okay)
|
|
40
|
+
tag.div class: 'dialog-footer' do
|
|
41
|
+
tag.button okay, type: 'button', class: 'btn btn-solid theme-secondary',
|
|
42
|
+
data: { bs_dismiss: 'dialog' }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module HouseAccount
|
|
2
|
+
module Helpers
|
|
3
|
+
# The page of a signup flow: the mark over the wall, a title, a line, one card, the way out.
|
|
4
|
+
module Flows
|
|
5
|
+
# How wide the card is unless a page says otherwise: one column of a form, on a desktop.
|
|
6
|
+
WIDTH = '30rem'
|
|
7
|
+
|
|
8
|
+
# Draws the page around the card's content, standing on the wall: the mark linking to
|
|
9
|
+
# `logo` over what the view said in `content_for` — its `:title`, its `:subtitle`, how
|
|
10
|
+
# wide the card is in `:width` — and the pieces of `footer` under the card joined by a
|
|
11
|
+
# middot. No subtitle draws no line and an empty footer no foot: a page says only what
|
|
12
|
+
# it has.
|
|
13
|
+
def flow(footer: [], logo: Logos::HOME, &)
|
|
14
|
+
parts = [
|
|
15
|
+
tag.header(houseaccount_logo(href: logo)),
|
|
16
|
+
*houseaccount_flow_words,
|
|
17
|
+
houseaccount_flow_card(content_for(:width), &),
|
|
18
|
+
houseaccount_flow_footer(footer),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
tag.div safe_join(parts.compact), class: 'flow', data: { controller: 'wall' }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def houseaccount_flow_words
|
|
27
|
+
[tag.h1(content_for(:title)), (tag.p content_for :subtitle if content_for? :subtitle)]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def houseaccount_flow_card(width, &)
|
|
31
|
+
width = width.presence || WIDTH
|
|
32
|
+
style = "--flow-width: #{width}" unless width == WIDTH
|
|
33
|
+
|
|
34
|
+
tag.div capture(&), class: 'flow-card p-3 md:p-4 mx-auto', style:
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def houseaccount_flow_footer(pieces)
|
|
38
|
+
pieces = Array(pieces).compact_blank
|
|
39
|
+
return if pieces.empty?
|
|
40
|
+
|
|
41
|
+
tag.footer safe_join(pieces, tag.span('·', class: 'fg-2', aria: { hidden: true }))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module HouseAccount
|
|
2
|
+
module Helpers
|
|
3
|
+
# What every HouseAccount page puts in its `<head>`.
|
|
4
|
+
module Heads
|
|
5
|
+
# One line per tag, each at the depth a head's children sit at in a layout indented
|
|
6
|
+
# by two: `<html>`, then `<head>`, then these.
|
|
7
|
+
INDENT = "\n "
|
|
8
|
+
|
|
9
|
+
# The stylesheet and the script every page needs, and the icons this app is known
|
|
10
|
+
# by. Both files at this version's own path, which never changes and so is kept for
|
|
11
|
+
# a year. A page refresh Turbo is sent morphs the page in place and keeps the scroll,
|
|
12
|
+
# so an answer landing in a thread leaves the reader where they were.
|
|
13
|
+
def houseaccount_head_tags
|
|
14
|
+
safe_join [
|
|
15
|
+
tag.meta(name: 'viewport', content: 'width=device-width, initial-scale=1'),
|
|
16
|
+
tag.meta(name: 'color-scheme', content: 'light dark'),
|
|
17
|
+
tag.meta(name: 'turbo-refresh-method', content: 'morph'),
|
|
18
|
+
tag.meta(name: 'turbo-refresh-scroll', content: 'preserve'),
|
|
19
|
+
tag.link(rel: 'stylesheet', href: "/v#{VERSION}/css/houseaccount.css"),
|
|
20
|
+
tag.script('', type: 'module', src: "/v#{VERSION}/js/houseaccount.js"),
|
|
21
|
+
*houseaccount_icon_tags,
|
|
22
|
+
], INDENT
|
|
23
|
+
end
|
|
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
|
|
32
|
+
mark = Rails.env.development? ? "#{app}-dev" : app
|
|
33
|
+
[
|
|
34
|
+
tag.link(rel: 'icon', href: "/favicon/#{mark}.svg", type: 'image/svg+xml'),
|
|
35
|
+
tag.link(rel: 'icon', href: "/favicon/#{app}-96.png", type: 'image/png', sizes: '96x96'),
|
|
36
|
+
tag.link(rel: 'apple-touch-icon', href: "/favicon/#{app}-180.png"),
|
|
37
|
+
tag.link(rel: 'manifest', href: "/favicon/#{app}.webmanifest"),
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|