proscenium 0.19.0.beta4-x86_64-darwin → 0.19.0.beta6-x86_64-darwin

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/proscenium/builder.rb +9 -13
  4. data/lib/proscenium/ext/proscenium +0 -0
  5. data/lib/proscenium/importer.rb +13 -13
  6. data/lib/proscenium/middleware/base.rb +5 -2
  7. data/lib/proscenium/middleware/engines.rb +5 -9
  8. data/lib/proscenium/middleware/esbuild.rb +13 -8
  9. data/lib/proscenium/middleware.rb +2 -4
  10. data/lib/proscenium/railtie.rb +15 -5
  11. data/lib/proscenium/react_componentable.rb +1 -1
  12. data/lib/proscenium/resolver.rb +3 -8
  13. data/lib/proscenium/side_load.rb +1 -1
  14. data/lib/proscenium/ui/flash/index.css +1 -0
  15. data/lib/proscenium/ui/flash/index.js +73 -0
  16. data/lib/proscenium/ui/flash.rb +15 -0
  17. data/lib/proscenium/ui/form/field_methods.rb +88 -0
  18. data/lib/proscenium/ui/form/fields/base.rb +188 -0
  19. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  20. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  21. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  22. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  23. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  24. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  25. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  26. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  27. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  28. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  29. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  30. data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
  31. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  32. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  33. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  34. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  35. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  36. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  37. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  38. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  39. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  40. data/lib/proscenium/ui/form/fields/select.rb +300 -0
  41. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  42. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  43. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  44. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  45. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  46. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  47. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  48. data/lib/proscenium/ui/form/translation.rb +71 -0
  49. data/lib/proscenium/ui/form.css +52 -0
  50. data/lib/proscenium/ui/form.rb +213 -0
  51. data/lib/proscenium/ui/props.css +7 -0
  52. data/lib/proscenium/ui/react-manager/index.jsx +1 -1
  53. data/lib/proscenium/ui/test.js +1 -1
  54. data/lib/proscenium/ui/ujs/index.js +1 -1
  55. data/lib/proscenium/ui.rb +3 -0
  56. data/lib/proscenium/utils.rb +33 -0
  57. data/lib/proscenium/version.rb +1 -1
  58. data/lib/proscenium/view_component.rb +0 -2
  59. data/lib/proscenium.rb +12 -2
  60. metadata +61 -10
  61. data/lib/proscenium/middleware/runtime.rb +0 -18
@@ -0,0 +1,297 @@
1
+ @layer proscenium-ui {
2
+ pui-tel-field {
3
+ --flag-ad: -24px 0;
4
+ --flag-ae: -48px 0;
5
+ --flag-af: -72px 0;
6
+ --flag-ag: -96px 0;
7
+ --flag-ai: -120px 0;
8
+ --flag-al: -144px 0;
9
+ --flag-am: -168px 0;
10
+ --flag-an: -192px 0;
11
+ --flag-ao: -216px 0;
12
+ --flag-ar: -240px 0;
13
+ --flag-as: -264px 0;
14
+ --flag-at: -288px 0;
15
+ --flag-au: -312px 0;
16
+ --flag-aw: -336px 0;
17
+ --flag-ax: -360px 0;
18
+ --flag-az: 0 -24px;
19
+ --flag-ba: -24px -24px;
20
+ --flag-bb: -48px -24px;
21
+ --flag-bd: -72px -24px;
22
+ --flag-be: -96px -24px;
23
+ --flag-bf: -120px -24px;
24
+ --flag-bg: -144px -24px;
25
+ --flag-bh: -168px -24px;
26
+ --flag-bi: -192px -24px;
27
+ --flag-bj: -216px -24px;
28
+ --flag-bl: -240px -24px;
29
+ --flag-bm: -264px -24px;
30
+ --flag-bn: -288px -24px;
31
+ --flag-bo: -312px -24px;
32
+ --flag-br: -336px -24px;
33
+ --flag-bs: -360px -24px;
34
+ --flag-bt: 0 -48px;
35
+ --flag-bw: -24px -48px;
36
+ --flag-by: -48px -48px;
37
+ --flag-bz: -72px -48px;
38
+ --flag-ca: -96px -48px;
39
+ --flag-cd: -120px -48px;
40
+ --flag-cf: -144px -48px;
41
+ --flag-cg: -168px -48px;
42
+ --flag-ch: -192px -48px;
43
+ --flag-ci: -216px -48px;
44
+ --flag-ck: -240px -48px;
45
+ --flag-cl: -264px -48px;
46
+ --flag-cm: -288px -48px;
47
+ --flag-cn: -312px -48px;
48
+ --flag-co: -336px -48px;
49
+ --flag-cr: -360px -48px;
50
+ --flag-cu: 0 -72px;
51
+ --flag-cv: -24px -72px;
52
+ --flag-cw: -48px -72px;
53
+ --flag-cy: -72px -72px;
54
+ --flag-cz: -96px -72px;
55
+ --flag-de: -120px -72px;
56
+ --flag-dj: -144px -72px;
57
+ --flag-dk: -168px -72px;
58
+ --flag-dm: -192px -72px;
59
+ --flag-do: -216px -72px;
60
+ --flag-dz: -240px -72px;
61
+ --flag-ec: -264px -72px;
62
+ --flag-ee: -288px -72px;
63
+ --flag-eg: -312px -72px;
64
+ --flag-eh: -336px -72px;
65
+ --flag-er: -360px -72px;
66
+ --flag-es: 0 -96px;
67
+ --flag-et: -24px -96px;
68
+ --flag-fi: -48px -96px;
69
+ --flag-fj: -72px -96px;
70
+ --flag-fk: -96px -96px;
71
+ --flag-fm: -120px -96px;
72
+ --flag-fo: -144px -96px;
73
+ --flag-fr: -168px -96px;
74
+ --flag-ga: -192px -96px;
75
+ --flag-gb: -216px -96px;
76
+ --flag-gd: -240px -96px;
77
+ --flag-ge: -264px -96px;
78
+ --flag-gg: -288px -96px;
79
+ --flag-gh: -312px -96px;
80
+ --flag-gi: -336px -96px;
81
+ --flag-gl: -360px -96px;
82
+ --flag-gm: 0 -120px;
83
+ --flag-gn: -24px -120px;
84
+ --flag-gq: -48px -120px;
85
+ --flag-gr: -72px -120px;
86
+ --flag-gt: -96px -120px;
87
+ --flag-gu: -120px -120px;
88
+ --flag-gw: -144px -120px;
89
+ --flag-gy: -168px -120px;
90
+ --flag-hk: -192px -120px;
91
+ --flag-hn: -216px -120px;
92
+ --flag-hr: -240px -120px;
93
+ --flag-ht: -264px -120px;
94
+ --flag-hu: -288px -120px;
95
+ --flag-id: -312px -120px;
96
+ --flag-ie: -336px -120px;
97
+ --flag-il: -360px -120px;
98
+ --flag-im: 0 -144px;
99
+ --flag-in: -24px -144px;
100
+ --flag-iq: -48px -144px;
101
+ --flag-ir: -72px -144px;
102
+ --flag-is: -96px -144px;
103
+ --flag-it: -120px -144px;
104
+ --flag-je: -144px -144px;
105
+ --flag-jm: -168px -144px;
106
+ --flag-jo: -192px -144px;
107
+ --flag-jp: -216px -144px;
108
+ --flag-ke: -240px -144px;
109
+ --flag-kg: -264px -144px;
110
+ --flag-kh: -288px -144px;
111
+ --flag-ki: -312px -144px;
112
+ --flag-km: -336px -144px;
113
+ --flag-kn: -360px -144px;
114
+ --flag-kp: 0 -168px;
115
+ --flag-kr: -24px -168px;
116
+ --flag-kw: -48px -168px;
117
+ --flag-ky: -72px -168px;
118
+ --flag-kz: -96px -168px;
119
+ --flag-la: -120px -168px;
120
+ --flag-lb: -144px -168px;
121
+ --flag-lc: -168px -168px;
122
+ --flag-li: -192px -168px;
123
+ --flag-lk: -216px -168px;
124
+ --flag-lr: -240px -168px;
125
+ --flag-ls: -264px -168px;
126
+ --flag-lt: -288px -168px;
127
+ --flag-lu: -312px -168px;
128
+ --flag-lv: -336px -168px;
129
+ --flag-ly: -360px -168px;
130
+ --flag-ma: 0 -192px;
131
+ --flag-mc: -24px -192px;
132
+ --flag-md: -48px -192px;
133
+ --flag-me: -72px -192px;
134
+ --flag-mf: -96px -192px;
135
+ --flag-mg: -120px -192px;
136
+ --flag-mh: -144px -192px;
137
+ --flag-mk: -168px -192px;
138
+ --flag-ml: -192px -192px;
139
+ --flag-mm: -216px -192px;
140
+ --flag-mn: -240px -192px;
141
+ --flag-mo: -264px -192px;
142
+ --flag-mp: -288px -192px;
143
+ --flag-mq: -312px -192px;
144
+ --flag-mr: -336px -192px;
145
+ --flag-ms: -360px -192px;
146
+ --flag-mt: 0 -216px;
147
+ --flag-mu: -24px -216px;
148
+ --flag-mv: -48px -216px;
149
+ --flag-mw: -72px -216px;
150
+ --flag-mx: -96px -216px;
151
+ --flag-my: -120px -216px;
152
+ --flag-mz: -144px -216px;
153
+ --flag-na: -168px -216px;
154
+ --flag-nc: -192px -216px;
155
+ --flag-ne: -216px -216px;
156
+ --flag-nf: -240px -216px;
157
+ --flag-ng: -264px -216px;
158
+ --flag-ni: -288px -216px;
159
+ --flag-nl: -312px -216px;
160
+ --flag-no: -336px -216px;
161
+ --flag-np: -360px -216px;
162
+ --flag-nr: 0 -240px;
163
+ --flag-nu: -24px -240px;
164
+ --flag-nz: -48px -240px;
165
+ --flag-om: -72px -240px;
166
+ --flag-pa: -96px -240px;
167
+ --flag-pe: -120px -240px;
168
+ --flag-pf: -144px -240px;
169
+ --flag-pg: -168px -240px;
170
+ --flag-ph: -192px -240px;
171
+ --flag-pk: -216px -240px;
172
+ --flag-pl: -240px -240px;
173
+ --flag-pn: -264px -240px;
174
+ --flag-pr: -288px -240px;
175
+ --flag-ps: -312px -240px;
176
+ --flag-pt: -336px -240px;
177
+ --flag-pw: -360px -240px;
178
+ --flag-py: 0 -264px;
179
+ --flag-qa: -24px -264px;
180
+ --flag-ro: -48px -264px;
181
+ --flag-rs: -72px -264px;
182
+ --flag-ru: -96px -264px;
183
+ --flag-rw: -120px -264px;
184
+ --flag-sa: -144px -264px;
185
+ --flag-sb: -168px -264px;
186
+ --flag-sc: -192px -264px;
187
+ --flag-sd: -216px -264px;
188
+ --flag-se: -240px -264px;
189
+ --flag-sg: -264px -264px;
190
+ --flag-sh: -288px -264px;
191
+ --flag-si: -312px -264px;
192
+ --flag-sk: -336px -264px;
193
+ --flag-sl: -360px -264px;
194
+ --flag-sm: 0 -288px;
195
+ --flag-sn: -24px -288px;
196
+ --flag-so: -48px -288px;
197
+ --flag-sr: -72px -288px;
198
+ --flag-ss: -96px -288px;
199
+ --flag-st: -120px -288px;
200
+ --flag-sv: -144px -288px;
201
+ --flag-sy: -168px -288px;
202
+ --flag-sz: -192px -288px;
203
+ --flag-tc: -216px -288px;
204
+ --flag-td: -240px -288px;
205
+ --flag-tg: -264px -288px;
206
+ --flag-th: -288px -288px;
207
+ --flag-tj: -312px -288px;
208
+ --flag-tk: -336px -288px;
209
+ --flag-tl: -360px -288px;
210
+ --flag-tm: 0 -312px;
211
+ --flag-tn: -24px -312px;
212
+ --flag-to: -48px -312px;
213
+ --flag-tr: -72px -312px;
214
+ --flag-tt: -96px -312px;
215
+ --flag-tv: -120px -312px;
216
+ --flag-tw: -144px -312px;
217
+ --flag-tz: -168px -312px;
218
+ --flag-ua: -192px -312px;
219
+ --flag-ug: -216px -312px;
220
+ --flag-us: -240px -312px;
221
+ --flag-uy: -264px -312px;
222
+ --flag-uz: -288px -312px;
223
+ --flag-va: -312px -312px;
224
+ --flag-vc: -336px -312px;
225
+ --flag-ve: -360px -312px;
226
+ --flag-vg: 0 -336px;
227
+ --flag-vi: -24px -336px;
228
+ --flag-vn: -48px -336px;
229
+ --flag-vu: -72px -336px;
230
+ --flag-wf: -96px -336px;
231
+ --flag-ws: -120px -336px;
232
+ --flag-ye: -144px -336px;
233
+ --flag-yt: -168px -336px;
234
+ --flag-za: -192px -336px;
235
+ --flag-zm: -216px -336px;
236
+ --flag-zw: -240px -336px;
237
+
238
+ input {
239
+ border: none;
240
+ background-color: var(--pui-input-background-color);
241
+ color: var(--pui-input-color);
242
+ }
243
+
244
+ [part="inputs"] {
245
+ border: var(--pui-input-border);
246
+ border-radius: var(--pui-input-border-radius);
247
+ display: inline-flex;
248
+ }
249
+
250
+ [part="country"] {
251
+ background-color: var(--pui-input-background-color);
252
+ display: flex;
253
+ align-items: center;
254
+ position: relative;
255
+ vertical-align: sub;
256
+ padding-left: 4px;
257
+ padding-right: 6px;
258
+
259
+ &::before {
260
+ content: "";
261
+ background: url("/proscenium/images/flags.png") no-repeat center;
262
+ background-position: var(--flag-position, 0 0);
263
+ width: 24px;
264
+ height: 24px;
265
+ }
266
+
267
+ &::after {
268
+ content: "\005E";
269
+ width: 1em;
270
+ height: 1em;
271
+ border-left: 1px solid var(--pui-input-accent-color);
272
+ transform: rotate(180deg);
273
+ text-align: center;
274
+ }
275
+
276
+ select {
277
+ position: absolute;
278
+ left: 0;
279
+ top: 0;
280
+ width: 100%;
281
+ height: 100%;
282
+ background-color: transparent;
283
+ border: none;
284
+ color: transparent;
285
+ box-shadow: none;
286
+ cursor: pointer;
287
+ appearance: none;
288
+ white-space: nowrap;
289
+
290
+ &:disabled {
291
+ opacity: 0.5;
292
+ cursor: default;
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,83 @@
1
+ import { Maskito } from "https://esm.sh/@maskito/core@2.2.0";
2
+ import { maskitoPhoneOptionsGenerator } from "https://esm.sh/@maskito/phone@2.2.0";
3
+ import parsePhoneNumber from "https://esm.sh/libphonenumber-js@1.10.60/min";
4
+ import metadata from "https://esm.sh/libphonenumber-js@1.10.60/min/metadata";
5
+
6
+ class TelField extends HTMLElement {
7
+ connectedCallback() {
8
+ this.$country = this.querySelector("[part='country']");
9
+ this.$select = this.querySelector("select");
10
+ this.$input = this.querySelector("input");
11
+
12
+ this.#initMask();
13
+ this.#setFlag(this.$select.querySelector("option:checked").value);
14
+
15
+ this.$select.addEventListener("change", this);
16
+ }
17
+
18
+ handleEvent(event) {
19
+ event.type === "change" && this.#onSelectChange(event);
20
+ }
21
+
22
+ #onSelectChange = ({ target }) => {
23
+ this.#setFlag(target.value);
24
+ this.#resetMask();
25
+ };
26
+
27
+ #setFlag(country) {
28
+ this.$country.style.setProperty(
29
+ "--flag-position",
30
+ `var(--flag-${country.toLowerCase()})`
31
+ );
32
+ }
33
+
34
+ #initMask() {
35
+ this.mask = new Maskito(
36
+ this.$input,
37
+ maskitoPhoneOptionsGenerator({
38
+ countryIsoCode: this.$select.value,
39
+ metadata,
40
+ })
41
+ );
42
+
43
+ if (this.$input.value !== "") {
44
+ const phone = parsePhoneNumber(this.$input.value, this.$select.value, {
45
+ extract: false,
46
+ });
47
+ this.$input.value = phone.format("INTERNATIONAL");
48
+
49
+ if (phone.country) {
50
+ this.$select.value = phone.country;
51
+ this.#setFlag(phone.country);
52
+ }
53
+ }
54
+ }
55
+
56
+ #resetMask() {
57
+ this.mask.destroy();
58
+ this.mask = new Maskito(
59
+ this.$input,
60
+ maskitoPhoneOptionsGenerator({
61
+ countryIsoCode: this.$select.value,
62
+ strict: true,
63
+ metadata,
64
+ })
65
+ );
66
+
67
+ // Update country prefix
68
+ if (this.$input.value !== "") {
69
+ let phone = parsePhoneNumber(this.$input.value, this.$select.value, {
70
+ extract: false,
71
+ });
72
+ phone = parsePhoneNumber(phone.nationalNumber, this.$select.value);
73
+ this.$input.value = phone.format("INTERNATIONAL");
74
+ }
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ this.mask.destroy();
79
+ this.$select.removeEventListener("change", this);
80
+ }
81
+ }
82
+
83
+ customElements.define("pui-tel-field", TelField);
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'phonelib'
4
+ require 'countries/iso3166'
5
+
6
+ module Proscenium::UI::Form::Fields
7
+ class Tel < Base
8
+ DEFAULT_COUNTRY = 'US'
9
+
10
+ sideload_assets js: { type: 'module' }
11
+
12
+ register_element :pui_tel_field
13
+
14
+ def initialize(attribute, model, form, type: nil, error: nil, **attributes) # rubocop:disable Metrics/ParameterLists
15
+ super
16
+
17
+ @default_country = @attributes.delete(:default_country)&.to_s&.upcase || DEFAULT_COUNTRY
18
+ end
19
+
20
+ def view_template
21
+ field :pui_tel_field do
22
+ label for: field_id
23
+
24
+ div part: :inputs do
25
+ div part: :country do
26
+ select do
27
+ countries.each do |name, code|
28
+ option(value: code, selected: code == country) { name }
29
+ end
30
+ end
31
+ end
32
+
33
+ input(name: field_name, type: 'text', part: :number, id: field_id, **build_attributes)
34
+ end
35
+
36
+ hint
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def country
43
+ @country ||= if value.blank?
44
+ @default_country
45
+ else
46
+ Phonelib.parse(value, @default_country).country || @default_country
47
+ end
48
+ end
49
+
50
+ def countries
51
+ ISO3166::Country.all_names_with_codes.to_h
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,50 @@
1
+ import clsx from 'clsx'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import dsx from '/hue/lib/hue/utils/dsx'
5
+ import { useFormError } from '../../hooks'
6
+
7
+ import styles from './index.module.css'
8
+
9
+ const Textarea = ({ label, hint, className, inputClassName, errorAttrName, ...props }) => {
10
+ const [error, hasError] = useFormError(errorAttrName || props.name)
11
+
12
+ return (
13
+ <div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
14
+ <label>
15
+ <span>
16
+ {label ? <span>{label}</span> : null}
17
+ {hasError ? <span>{error}</span> : null}
18
+ </span>
19
+
20
+ <textarea className={inputClassName || styles.input} {...props} />
21
+ </label>
22
+
23
+ {hint ? <div className={styles.hint}>{hint}</div> : null}
24
+ </div>
25
+ )
26
+ }
27
+
28
+ Textarea.displayName = 'Hue.Form.Fields.Textarea'
29
+ Textarea.propTypes = {
30
+ name: PropTypes.string.isRequired,
31
+
32
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]),
33
+
34
+ // Custom class name. This will be appended to the default class.
35
+ className: PropTypes.string,
36
+
37
+ // Custom class name for the actual textarea element. This will replace the default class.
38
+ inputClassName: PropTypes.string,
39
+
40
+ // The name of the attribute to use for the error message. Default: 'props.name'.
41
+ errorAttrName: PropTypes.string,
42
+
43
+ id: PropTypes.string,
44
+ hint: PropTypes.string,
45
+ disabled: PropTypes.bool
46
+
47
+ // All remaining non-descript props will be forwarded to the <input> element.
48
+ }
49
+
50
+ export default Textarea
@@ -0,0 +1,13 @@
1
+ @layer hue-component {
2
+ .fieldWrapper {
3
+ @mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
4
+ }
5
+
6
+ .input {
7
+ @mixin textarea from url('/hue/lib/hue/mixins/textarea.mixin.css');
8
+ }
9
+
10
+ .hint {
11
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import Textarea from '../'
2
+
3
+ const Component = () => {
4
+ return <Textarea name="name" label="Name" hint="Some hint about the input" />
5
+ }
6
+ Component.displayName = 'Hue.Form.Fields.Textarea.Previews.Basic'
7
+
8
+ export default Component
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class Textarea < Base
5
+ register_element :pui_textarea
6
+
7
+ def view_template
8
+ field :pui_textarea do
9
+ label do
10
+ attrs = build_attributes
11
+ value = attrs.delete(:value)
12
+ textarea(name: field_name, **attrs) { value }
13
+ end
14
+ hint
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Translation
4
+ # Lookup translations for the given namespace using I18n, based on model name, and attribute name.
5
+ #
6
+ # Lookup priority with nested attributes:
7
+ #
8
+ # form.{namespace}.{model}/{attribute.first}.{attribute.last}
9
+ # form.{namespace}.{attribute.first}.{attribute.last}
10
+ # form.{namespace}.{model}.{attribute.last}
11
+ # form.{namespace}.defaults.{attribute.last}
12
+ # {default}
13
+ #
14
+ # Lookup priority without nested attributes:
15
+ #
16
+ # form.{namespace}.{model}.{attribute}
17
+ # form.{namespace}.defaults.{attribute}
18
+ # {default}
19
+ #
20
+ # Namespace is used for :labels and :hints.
21
+ #
22
+ # Model is the actual object name, for a @user object you'll have :user.
23
+ # Attribute is the attribute itself, :name for example, or [:user, :name] if nested.
24
+ #
25
+ # If :postfix is given, it will be appended to the end of each lookup entry. So with a :postfix of
26
+ # 'stuff', '_stuff' will be appended:
27
+ #
28
+ # form.{namespace}.{model}.{attribute}_{postfix}
29
+ #
30
+ def translate(namespace, attribute, postfix: nil, default: '')
31
+ lookups = []
32
+ postfix = "_#{postfix}" if postfix
33
+ model_key = model.model_name.i18n_key
34
+
35
+ if attribute.is_a?(Array) && attribute.length > 1
36
+ joined_attrs = attribute.join('.')
37
+ lookups << :"#{model_key}/#{joined_attrs}#{postfix}"
38
+ lookups << :"#{joined_attrs}#{postfix}"
39
+ lookups << :"defaults.#{attribute.last}#{postfix}"
40
+ else
41
+ attribute = attribute.first if attribute.is_a?(Array)
42
+ lookups << :"#{model_key}.#{attribute}#{postfix}"
43
+ lookups << :"defaults.#{attribute}#{postfix}"
44
+ end
45
+
46
+ lookups << default
47
+
48
+ I18n.t(lookups.shift, scope: :"#{i18n_scope}.#{namespace}",
49
+ default: lookups).presence
50
+ end
51
+
52
+ def translate_label(attribute, default: nil, postfix: nil)
53
+ unless default
54
+ model = @model.class
55
+
56
+ if @model.class.respond_to?(:reflect_on_association) && attribute.count > 1
57
+ model = @model.class.reflect_on_association(attribute.first).klass
58
+ end
59
+
60
+ default = model.human_attribute_name(attribute.last)
61
+ end
62
+
63
+ translate :labels, attribute, default:, postfix:
64
+ end
65
+
66
+ private
67
+
68
+ def i18n_scope
69
+ 'form'
70
+ end
71
+ end
@@ -0,0 +1,52 @@
1
+ @import "/proscenium/props.css";
2
+
3
+ pui-field {
4
+ display: block;
5
+
6
+ [part="error"] {
7
+ &::before {
8
+ content: " ";
9
+ }
10
+
11
+ color: red;
12
+ }
13
+
14
+ [part="hint"] {
15
+ font-size: 0.9em;
16
+ margin-top: 0.2em;
17
+ }
18
+
19
+ input {
20
+ border: var(--pui-input-border);
21
+ border-radius: var(--pui-input-border-radius);
22
+ background-color: var(--pui-input-background-color);
23
+ }
24
+ }
25
+
26
+ pui-checkbox {
27
+ display: block;
28
+
29
+ label {
30
+ display: flex;
31
+ gap: 0.3rem;
32
+ }
33
+ }
34
+
35
+ pui-radio-group {
36
+ [part="radio_group_inputs"] {
37
+ display: flex;
38
+ gap: 1em;
39
+
40
+ > label > input {
41
+ margin-right: 0.3em;
42
+ }
43
+ }
44
+ }
45
+
46
+ pui-textarea {
47
+ display: block;
48
+
49
+ textarea {
50
+ display: block;
51
+ }
52
+ }