protos 0.4.3 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7b9c6eb83f7c89f66d1f625fd200b2f6595c098130e16da1fcb6b2e96dd59fe
4
- data.tar.gz: 07a7e91d1ef7041dba38b72eb412309d31cf6426ad1e3b157f6a83fab59a32dd
3
+ metadata.gz: '0796561a80e75fb39f8b35ec75257104adc90b21227ef1525876576d10415269'
4
+ data.tar.gz: 394a8ee190d1d2647476db45d2d9b9edb156bc42a3935898d6f1deca35261326
5
5
  SHA512:
6
- metadata.gz: 36fa9e7809d68b3d1227814cb7bb23fc44066813de58360486f5c7ffffafb30f58f6cb50309aef8902796ea234dfe3f5dcd46b451673cf77600da5f257840e33
7
- data.tar.gz: 8ee74d55eebfaaec0d488809563695bf456229ed8dad466a10b6b5f20c117ed02644ef2a7a9646679a47697cae06eca5e6e50f21a863292d46c9753be4c82939
6
+ metadata.gz: 59e2288d2ae8adfc3b352838dda25c776edddf48d99b310181629e78f09831552be23e36adedaca7bf3601678cb08950500f4b72f2ed609324ccd72ddd32a92d
7
+ data.tar.gz: e0e4beda7e4cbff4e3b669899b65df2ec7c37fe388e75b561418ce1cc65abd4cb1cfecabd743f964bf0dee98de1c4ccdbf9f7e76f77078f294a8bfb0a4272802
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2024-09-04
4
+
5
+ - Changes how merging attributes works to only mix on certain attributes,
6
+ overriding on all others. This is opposite to how attributes used to be merged
7
+ by default. This is a fix for attributes like `value` where you actually need
8
+ to override them.
9
+ - Adds tests for all Rails components
10
+ - Adds a separate tested `Mix` class for handling attribute merging
11
+
12
+ ## [0.5.0] - 2024-08-27
13
+
14
+ - Fixes all accessibility violations according to Axe Core
15
+ - Reduces responsibility of Tabs to only be a tablist, no tab panels
16
+ - Fixes passing ID to popovers, dropdowns, drawers, etc to not override the
17
+ input ID
18
+ - Changes trigger on popover to be a button instead of a div
19
+
3
20
  ## [0.4.3] - 2024-08-14
4
21
 
5
22
  - Removes unneeded auto-loading in Rails which fixes collisions with `protos-markdown`
data/README.md CHANGED
@@ -20,10 +20,90 @@ Other Phlex based UI libraries worth checking out:
20
20
  - [PhlexUI](https://phlexui.com/)
21
21
  - [ZestUI](https://zestui.com/)
22
22
 
23
+ Thinking of making your next static site using Phlex? Check out
24
+ [staticky](https://github.com/nolantait/staticky). The protos docs were
25
+ published using it.
26
+
27
+ ## Phlex components
28
+
29
+ Phlex is a fantastic framework for building frontend components in pure Ruby:
30
+
31
+ ```ruby
32
+ class Navbar
33
+ def view_template
34
+ header(class: "flex items-center justify-between") do
35
+ h3 { "My site" }
36
+ button { "Log out" }
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ But how can we sometimes render this `Navbar` with a different background color?
43
+
44
+ It would be nice to have our components take a class like any other element:
45
+
46
+ ```ruby
47
+ render Navbar.new(class: "bg-primary")
48
+ ```
49
+
50
+ Unfortunately `class` is a special keyword in Ruby, so we need to do some
51
+ awkward handling to use it like this:
52
+
53
+ ```ruby
54
+ class Navbar
55
+ def initialize(**options)
56
+ # Keyword `class` is a special word in Ruby so we have to awkwardly unwrap
57
+ # like this instead of using keyword arguments
58
+ @classes = options[:class]
59
+ end
60
+
61
+ def view_template
62
+ header(class: "#{@classes} flex items-center justify-between") do
63
+ h3 { "My site" }
64
+ button { "Log out" }
65
+ end
66
+ end
67
+ end
68
+ ```
69
+
70
+ Now we can pass in a style to our container, but what about overriding the style
71
+ of the `h3` tag?
72
+
73
+ ```ruby
74
+ class Navbar
75
+ def initialize(**options)
76
+ # Keyword `class` is a special word in Ruby so we have to awkwardly unwrap
77
+ # like this instead of using keyword arguments
78
+ @container_classes = options[:class]
79
+ @title_classes = options[:title_class]
80
+ end
81
+
82
+ def view_template
83
+ header(class: "#{@classes} flex items-center justify-between") do
84
+ h3(class: @title_classes) { "My site" }
85
+ button { "Log out" }
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ Eventually everyone makes a kind of ad-hoc system for specifying styles. It gets
92
+ more complicated when you have attributes like a data-controller. How do you
93
+ give a good experience letting people using your components to add their own
94
+ controllers while your component depends on one already?
95
+
96
+ This library is an attempt to make this kind of developer experience while
97
+ making reusable components more convention over configuration.
98
+
23
99
  ## Protos::Component
24
100
 
25
- A protos component follows some conventions that make them easy to work with as
26
- components in your app.
101
+ A protos component follows 3 conventions that make them easy to work with as
102
+ components in your app:
103
+
104
+ - [Slots and themes](#slots-and-themes)
105
+ - [Attrs and default attrs](#attrs-and-default-attrs)
106
+ - [Params and options](#params-and-options)
27
107
 
28
108
  Every UI component library will have a tension between being too general to fit
29
109
  in your app or too narrow to be useful. Making components that look good out of
@@ -32,20 +112,18 @@ the box can make them hard to customize.
32
112
  We try and resolve this tension by making these components have a minimal style
33
113
  that can be easily overridden using some ergonomic conventions.
34
114
 
35
- There are 3 core conventions:
36
- - [Slots and themes](#slots-and-themes)
37
- - [Attrs and default attrs](#attrs-and-default-attrs)
38
- - [Params and options](#params-and-options)
39
-
40
115
  ### Slots and themes
41
116
 
42
- Components are styled with `css` slots that are filled with values from
43
- a `theme`.
117
+ Components are styled with `css` slots that get their values from a simple hash
118
+ we call a `theme`.
119
+
120
+ You define a `theme` for your component by defining a `#theme` method that
121
+ returns a hash.
122
+
123
+ Users of your components can override, merge, or remove parts of your theme by
124
+ passing in their own as an argument to the component. Another nice benefit is
125
+ that your markup doesn't get overwhelmed horizontally with your css classes.
44
126
 
45
- You define a theme for your component by defining a `#theme` method that returns
46
- a hash. This hash will be merged with any theme provided when rendering your
47
- component. This allows you to easily override styles for your components
48
- depending on their context.
49
127
 
50
128
  ```ruby
51
129
  class List < Protos::Component
@@ -58,7 +136,7 @@ class List < Protos::Component
58
136
 
59
137
  def theme
60
138
  {
61
- list: tokens("space-y-4"), # We can use `#tokens` from phlex (recommended)
139
+ list: "space-y-4", # We can use `#tokens` from phlex (recommended)
62
140
  item: "font-bold text-2xl" # Or just plain old strings
63
141
  }
64
142
  end
@@ -66,7 +144,10 @@ end
66
144
  ```
67
145
 
68
146
  Using a theme and css slots allows us to easily override any part of a component
69
- when we render:
147
+ when we render.
148
+
149
+ Here we are passing in our own theme. The default behavior is to add these
150
+ styles on to the theme, rather than replacing them.
70
151
 
71
152
  ```ruby
72
153
  render List.new(
@@ -77,7 +158,12 @@ render List.new(
77
158
  )
78
159
  ```
79
160
 
80
- This would combine the default and our theme using tailwind\_merge:
161
+ When the component is rendered the `tailwind_merge` gem will also prune any
162
+ duplicate unneeded styles.
163
+
164
+ For example even though the themes `list` key would be added together to become
165
+ `space-y-4 space-y-8`, the `tailwind_merge` gem will prune it down to just
166
+ `space-y-8` as the two styles conflict.
81
167
 
82
168
  ```html
83
169
  <ul class="space-y-8">
@@ -119,8 +205,8 @@ The new `css[:item]` slot would be:
119
205
  <li class="font-bold">Item 1</li>
120
206
  ```
121
207
 
122
- If you want to change the method we define our default theme you can override the
123
- `theme_method` on the class:
208
+ If you want to change the method we define our default theme under you can
209
+ override the `theme_method` on the class:
124
210
 
125
211
  ```ruby
126
212
  class List < Protos::Component
@@ -142,12 +228,15 @@ end
142
228
  ### Attrs and default attrs
143
229
 
144
230
  By convention, all components spread in an `attrs` hash on their outermost
145
- element of the component.
231
+ element of the component. There is no rule for this, but it makes them feel more
232
+ naturally like native html elements when you render them.
146
233
 
147
- By doing this we enable 2 main conveniences:
234
+ By doing this we enable 3 main conveniences:
148
235
  1. We can pass a `class` keyword when initializing the component which will be
149
236
  merged safely into the `css[:container]` slot
150
- 2. We can add default attributes that are safely merged with any provided to the
237
+ 2. We can pass any html attributes we want to the element such as `id`, `data`
238
+ etc and it will just work
239
+ 3. We can add default attributes that are safely merged with any provided to the
151
240
  component when its being initialized
152
241
 
153
242
  ```ruby
@@ -175,12 +264,15 @@ class List < Protos::Component
175
264
  end
176
265
  ```
177
266
 
178
- `#attrs` will by default merge the `class` keyword into the `css[:container]`
179
- slot which we define in our theme.
267
+ `#attrs` returns a hash which will by default merge the `class` keyword into the
268
+ `css[:container]` slot which we define in our theme. The `ul` elements class
269
+ would be `space-y-4` as that is the `css[:container]` on our theme.
270
+
271
+ Special html options (`class`, `data`) will be safely merged.
180
272
 
181
- Special html options will be safely merged. For examples, the component above
182
- defines a list controller. If we passed our own controller into data when we
183
- initialize, the component's data-controller attribute would be appended to:
273
+ For examples, the component above defines a list controller. If we passed our
274
+ own controller into data when we initialize, the component's data-controller
275
+ attribute would be appended to:
184
276
 
185
277
  ```ruby
186
278
  render List.new(
@@ -194,6 +286,9 @@ That would output both controllers to the DOM element:
194
286
  <ul data-controller="list tooltip">
195
287
  ```
196
288
 
289
+ This makes it very convenient to add functionality to basic components without
290
+ overriding their core behavior or having to modify/override their class.
291
+
197
292
  If we wanted to, just like for our theme we can change the method from
198
293
  `default_attrs` by defining the `default_attrs_method` on the class:
199
294
 
@@ -224,25 +319,27 @@ class List < Protos::Component
224
319
  end
225
320
  ```
226
321
 
322
+ This makes our initialization declarative and easy to extend without having to
323
+ consider how to call `super` in the initializer.
324
+
227
325
  The following keywords are reserved in the base class:
228
326
 
229
327
  - `class`
230
328
  - `theme`
231
329
  - `html_options`
232
330
 
331
+ You are free to add whatever positional or keyword arguments you like as long as
332
+ they don't directly conflict with those names.
333
+
233
334
  ## Putting it all together
234
335
 
235
- Here is an example of a small navbar component:
336
+ Lets revisit the example of our `Navbar` component:
236
337
 
237
338
  ```ruby
238
339
  require "protos"
239
340
 
240
341
  class Navbar < Protos::Component
241
342
  def view_template
242
- # **attrs will add:
243
- # - Any html options defined on the component initialization such as data,
244
- # role, for, etc..
245
- # - Class will be added to the css[:container] and applied
246
343
  header(**attrs) do
247
344
  h1(class: css[:heading]) { "Hello world" }
248
345
  h2(class: css[:subtitle]) { "With a subtitle" }
@@ -259,19 +356,19 @@ class Navbar < Protos::Component
259
356
 
260
357
  def theme
261
358
  {
262
- container: tokens(
263
- "flex",
264
- "justify-between",
265
- "items-center",
266
- "gap-sm"
267
- ),
268
- heading: tokens("text-2xl", "font-bold"),
269
- subtitle: tokens("text-base")
359
+ container: "flex justify-between items-center gap-sm",
360
+ heading: "text-2xl font-bold",
361
+ subtitle: "text-sm"
270
362
  }
271
363
  end
272
364
  end
365
+ ```
273
366
 
274
- component = Navbar.new(
367
+ Now all the concerns about adding in our behavior, styles, etc are handled for
368
+ us by convention:
369
+
370
+ ```ruby
371
+ render Navbar.new(
275
372
  # This will add to the component's css[:container] slot
276
373
  class: "my-sm",
277
374
  # This will add the controller and not remove
@@ -283,8 +380,6 @@ component = Navbar.new(
283
380
  subtitle!: "text-xl" # We can override the entire slot
284
381
  }
285
382
  )
286
-
287
- puts component.call
288
383
  ```
289
384
 
290
385
  Which produces the following html:
@@ -308,7 +403,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
308
403
 
309
404
  ## Usage
310
405
 
311
- Setup [Tailwindcss](https://tailwindcss.com/), [daisyUI](https://daisyui.com)
406
+ Setup [TailwindCSS](https://tailwindcss.com/), [DaisyUI](https://daisyui.com)
312
407
  and add the protos path to your content.
313
408
 
314
409
  ```
@@ -316,7 +411,7 @@ npm install -D tailwindcss postcss autoprefixer daisyui
316
411
  npx tailwindcss init
317
412
  ```
318
413
 
319
- Then we need to add the protos path to the `content` of our tailwindcss config
414
+ Then we need to add the protos path to the `content` of our tailwind config
320
415
  so tailwind will read the styles defined in the Protos gem.
321
416
 
322
417
  Protos also uses semantic spacing such as `p-sm` or `m-md` instead of set
@@ -344,15 +439,6 @@ module.exports = {
344
439
  lg: "var(--spacing-lg)",
345
440
  xl: "var(--spacing-xl)",
346
441
  },
347
- // If you use % based spacing you might want different spacing
348
- // for any vertical gaps to prevent overflow
349
- gap: {
350
- xs: "var(--spacing-gap-xs)",
351
- sm: "var(--spacing-gap-sm)",
352
- md: "var(--spacing-gap-md)",
353
- lg: "var(--spacing-gap-lg)",
354
- xl: "var(--spacing-gap-xl)",
355
- },
356
442
  },
357
443
  }
358
444
  // ....
@@ -388,11 +474,12 @@ end
388
474
 
389
475
  ## Building your own components
390
476
 
391
- You can override components simply by redefining the class in your own app:
477
+ You can override components simply by redefining sub-classing the class in your
478
+ own app:
392
479
 
393
480
  ```ruby
394
- module Protos
395
- class Swap < Component
481
+ module Components
482
+ class Swap < Protos::Component
396
483
  private
397
484
 
398
485
  def on(...)
@@ -408,8 +495,22 @@ module Protos
408
495
  end
409
496
  ```
410
497
 
411
- You could also encapsulate these more primitive proto components into your own
412
- components. You could use `Proto::List` to create your own list and even use
498
+ But its much better to avoid the sub-classing and just render the component
499
+ inside of your own:
500
+
501
+ ```ruby
502
+ module Components
503
+ class Swap < ApplicationComponent
504
+ def view_template
505
+ render Protos::Swap.new do |c|
506
+ # ....
507
+ end
508
+ end
509
+ end
510
+ end
511
+ ```
512
+
513
+ You could use `Proto::List` to create your own list and even use
413
514
  `Phlex::DeferredRender` to make the API more convenient.
414
515
 
415
516
  Let's create a list component with headers and actions:
@@ -5,16 +5,16 @@ module Protos
5
5
  class Item < Component
6
6
  # DOCS: An accorion is just a collapse with radio buttons.
7
7
 
8
- option :id, type: Types::Coercible::String
8
+ option :input_id, type: Types::String | Types::Integer
9
9
 
10
10
  def view_template(&block)
11
11
  li(**attrs) do
12
- render Collapse.new(theme: collapse_theme) do
13
- # form: "" prevents the radio button from being submitted if its
14
- # within a form
15
- input(type: :radio, name: id, form: "", autocomplete: :off)
16
- yield if block
17
- end
12
+ render Collapse.new(
13
+ theme: collapse_theme,
14
+ input_id: @input_id.to_s,
15
+ input_type: :radio,
16
+ &block
17
+ )
18
18
  end
19
19
  end
20
20
 
@@ -7,20 +7,30 @@ module Protos
7
7
  # to be open at the same time, use the collapse component.
8
8
  # https://daisyui.com/components/accordion/
9
9
 
10
- option :id, default: -> { "collapse-#{SecureRandom.hex(4)}" }
11
-
12
10
  def view_template(&)
13
11
  ul(**attrs, &)
14
12
  end
15
13
 
16
- def item(*, **, &) = render Item.new(*, id:, **, &)
14
+ def item(*, input_id: nil, **, &)
15
+ self.current_input_id = input_id
16
+
17
+ render Item.new(*, input_id: current_input_id, **, &)
18
+ end
17
19
 
18
20
  def content(...) = render Collapse::Content.new(...)
19
21
 
20
- def title(*, **, &) = render Collapse::Title.new(*, id:, **, &)
22
+ def title(*, **, &)
23
+ render Collapse::Title.new(*, input_id: current_input_id, **, &)
24
+ end
21
25
 
22
26
  private
23
27
 
28
+ attr_reader :current_input_id
29
+
30
+ def current_input_id=(value)
31
+ @current_input_id = value || "collapse-#{SecureRandom.hex(4)}"
32
+ end
33
+
24
34
  def theme
25
35
  {
26
36
  container: "join join-vertical"
@@ -11,6 +11,12 @@ module Protos
11
11
 
12
12
  private
13
13
 
14
+ def default_attrs
15
+ {
16
+ aria_label: "alert-actions"
17
+ }
18
+ end
19
+
14
20
  def theme
15
21
  {
16
22
  container: "flex gap-xs items-center"
@@ -4,11 +4,6 @@ module Protos
4
4
  class Attributes
5
5
  # DOCS: A class that represents the attributes of a component. This would be
6
6
  # all html options except for `class` and `theme`.
7
- #
8
- # This class is responsible for safely merging in both user supplied and
9
- # default attributes. When a user adds { data: { controller: "foo" }} to
10
- # their component. This will merge the value in so that any default
11
- # controllers do not get overridden.
12
7
 
13
8
  def initialize(attrs = {}, **kwargs)
14
9
  @attrs = attrs.merge!(kwargs)
@@ -34,19 +29,7 @@ module Protos
34
29
  private
35
30
 
36
31
  def mix(hash, *hashes)
37
- hashes.each_with_object(hash) do |hash, result|
38
- result.merge!(hash) do |_key, a, b| # rubocop:disable Metrics/ParameterLists
39
- next a unless b
40
- next a if a == b
41
-
42
- case [a, b]
43
- in String, String then "#{a} #{b}"
44
- in Array, Array then a + b
45
- in Hash, Hash then mix(a, b)
46
- else b
47
- end
48
- end
49
- end
32
+ Mix.call(hash, *hashes)
50
33
  end
51
34
  end
52
35
  end
@@ -15,6 +15,12 @@ module Protos
15
15
 
16
16
  private
17
17
 
18
+ def default_attrs
19
+ {
20
+ aria_label: "breadcrumbs"
21
+ }
22
+ end
23
+
18
24
  def theme
19
25
  {
20
26
  container: "breadcrumbs"
@@ -5,14 +5,15 @@ module Protos
5
5
  class Title < Component
6
6
  # DOCS: The title of a collapse. This is the content that is always
7
7
  # visible and is used to toggle the collapse.
8
- option :id,
9
- type: Types::Coercible::String,
8
+
9
+ option :input_id,
10
+ type: Types::String | Types::Integer | Types::Nil,
10
11
  reader: false,
11
- default: -> { "" }
12
+ default: -> { }
12
13
 
13
14
  def view_template(&)
14
- if @id.size.positive?
15
- label(for: @id, **attrs, &)
15
+ if @input_id
16
+ label(for: @input_id.to_s, **attrs, &)
16
17
  else
17
18
  div(**attrs, &)
18
19
  end
@@ -6,19 +6,30 @@ module Protos
6
6
  # is visible at all times, and the content is only visible when expanded.
7
7
  # https://daisyui.com/components/collapse/
8
8
 
9
- option :checkbox, default: -> { false }, type: Types::Bool, reader: false
10
- option :id,
9
+ option :input_type, default: -> { :checkbox }, reader: false
10
+ option :input_id,
11
+ reader: false,
11
12
  default: -> { "collapse-#{SecureRandom.hex(4)}" },
12
13
  type: Types::String
13
14
 
14
15
  def view_template
15
16
  div(**attrs) do
16
- input(type: "checkbox", id:, autocomplete: :off) if @checkbox
17
+ if @input_type
18
+ input(
19
+ type: @input_type,
20
+ id: @input_id,
21
+ name: @input_id,
22
+ autocomplete: :off,
23
+ # form: "" prevents the radio button from being submitted if its
24
+ # within a form
25
+ form: ""
26
+ )
27
+ end
17
28
  yield if block_given?
18
29
  end
19
30
  end
20
31
 
21
- def title(*, **, &) = render Title.new(*, id:, **, &)
32
+ def title(*, **, &) = render Title.new(*, input_id: @input_id, **, &)
22
33
 
23
34
  def content(...) = render Content.new(...)
24
35
 
@@ -12,7 +12,7 @@ module Protos
12
12
  }
13
13
 
14
14
  def view_template(&block)
15
- div(**attrs) do
15
+ li(**attrs) do
16
16
  label(class: css[:label]) do
17
17
  div(class: css[:icon], &block) if block
18
18
  input(
@@ -7,7 +7,7 @@ module Protos
7
7
  # inside a Protos::Command::Group component.
8
8
 
9
9
  def view_template(&)
10
- h2(**attrs, &)
10
+ li { h2(**attrs, &) }
11
11
  end
12
12
 
13
13
  private
@@ -27,14 +27,14 @@ module Protos
27
27
  option :html_options, default: -> { {} }, reader: false
28
28
 
29
29
  # Adds non-defined options to the html_options hash
30
- def initialize(*args, **kwargs, &)
30
+ def initialize(*, **kwargs, &)
31
31
  defined_keys = self.class.dry_initializer.definitions.keys
32
32
  defined, undefined =
33
33
  kwargs
34
34
  .partition { |key, _| defined_keys.include?(key) }
35
35
  .map!(&:to_h)
36
36
 
37
- super(*args, html_options: undefined, **defined, &)
37
+ super(*, html_options: undefined, **defined, &)
38
38
  end
39
39
 
40
40
  private
@@ -6,11 +6,15 @@ module Protos
6
6
  # DOCS: The content that will be shown when you open the drawer using the
7
7
  # trigger. This would be hidden until triggered.
8
8
 
9
- option :id, type: Types::Coercible::String
9
+ option :input_id, reader: false, type: Types::String
10
10
 
11
11
  def view_template
12
12
  aside(**attrs) do
13
- label(for: id, aria_label: "close sidebar", class: css[:toggle])
13
+ label(
14
+ for: @input_id,
15
+ aria_label: "close sidebar",
16
+ class: css[:toggle]
17
+ )
14
18
  yield if block_given?
15
19
  end
16
20
  end
@@ -6,10 +6,10 @@ module Protos
6
6
  # DOCS: The trigger for a drawer. When this is clicked the side will open
7
7
  # and overlap the main content with a darker overlay.
8
8
 
9
- option :id, type: Types::Coercible::String
9
+ option :input_id, reader: false, type: Types::String
10
10
 
11
11
  def view_template(&)
12
- label(for: id, **attrs, &)
12
+ label(for: @input_id, **attrs, &)
13
13
  end
14
14
 
15
15
  private
data/lib/protos/drawer.rb CHANGED
@@ -7,20 +7,29 @@ module Protos
7
7
  # trigger is clicked.
8
8
  # https://daisyui.com/components/drawer/
9
9
 
10
- option :id, type: Types::Coercible::String
10
+ option :id,
11
+ reader: false,
12
+ type: Types::Coercible::String,
13
+ default: -> { "drawer-#{SecureRandom.hex(4)}" }
11
14
 
12
15
  def view_template
13
16
  div(**attrs) do
14
- input(id:, type: :checkbox, class: css[:toggle], autocomplete: :off)
17
+ input(
18
+ id: @id,
19
+ type: :checkbox,
20
+ class: css[:toggle],
21
+ autocomplete: :off,
22
+ form: ""
23
+ )
15
24
  yield if block_given?
16
25
  end
17
26
  end
18
27
 
19
28
  def content(...) = render Content.new(...)
20
29
 
21
- def side(*, **, &) = render Side.new(*, id:, **, &)
30
+ def side(*, **, &) = render Side.new(*, input_id: @id, **, &)
22
31
 
23
- def trigger(*, **, &) = render Trigger.new(*, id:, **, &)
32
+ def trigger(*, **, &) = render Trigger.new(*, input_id: @id, **, &)
24
33
 
25
34
  private
26
35
 
data/lib/protos/mix.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protos
4
+ class Mix
5
+ # DOCS: This class is responsible for safely merging in both user supplied
6
+ # and default attributes. When a user adds { data: { controller: "foo" }} to
7
+ # their component. This will merge the value in so that any default
8
+ # controllers do not get overridden.
9
+
10
+ MERGEABLE_ATTRIBUTES = Set.new(%i[class data]).freeze
11
+
12
+ def self.call(...) = new.call(...)
13
+
14
+ def call(old_hash, *hashes)
15
+ hashes
16
+ .compact
17
+ .each_with_object(old_hash) do |new_hash, result|
18
+ merge(result, new_hash, top_level: true)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def merge(old_hash, new_hash, top_level: false) # rubocop:disable Metrics/PerceivedComplexity
25
+ old_hash.merge!(new_hash) do |key, old, new|
26
+ next old unless new
27
+ next old if old == new
28
+ next new if top_level && !MERGEABLE_ATTRIBUTES.include?(key)
29
+
30
+ case [old, new]
31
+ in String, String then "#{old} #{new}"
32
+ in Array, Array then old + new
33
+ in Hash, Hash then merge(old, new)
34
+ else new
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,7 +7,7 @@ module Protos
7
7
  # or click on to show the popover.
8
8
 
9
9
  def view_template(&)
10
- div(**attrs, &)
10
+ button(**attrs, &)
11
11
  end
12
12
 
13
13
  private
@@ -15,7 +15,8 @@ module Protos
15
15
  def default_attrs
16
16
  {
17
17
  data: { "protos--popover-target": "trigger" },
18
- tabindex: 0
18
+ tabindex: 0,
19
+ type: :button
19
20
  }
20
21
  end
21
22
  end
data/lib/protos/swap.rb CHANGED
@@ -8,7 +8,13 @@ module Protos
8
8
 
9
9
  def view_template
10
10
  label(**attrs) do
11
- input(type: :checkbox, class: css[:input], autocomplete: :off)
11
+ input(
12
+ type: :checkbox,
13
+ class: css[:input],
14
+ autocomplete: :off,
15
+ form: "",
16
+ aria_label: "swap"
17
+ )
12
18
  yield if block_given?
13
19
  end
14
20
  end
@@ -5,35 +5,24 @@ module Protos
5
5
  class Tab < Component
6
6
  # DOCS: A single tab in a tabs component
7
7
 
8
- option :id
9
- option :label
10
8
  option :active, default: -> { false }
11
9
  option :disabled, default: -> { false }
12
10
 
13
11
  def view_template(&)
14
- input(
15
- type: :radio,
16
- class: css[:input],
17
- name: id,
18
- role: :tab,
19
- aria_label: label,
20
- autocomplete: :off
21
- )
22
- div(**attrs, &)
12
+ button(**attrs, disabled:, &)
23
13
  end
24
14
 
25
15
  private
26
16
 
27
17
  def default_attrs
28
18
  {
29
- role: :tabpanel
19
+ role: :tab
30
20
  }
31
21
  end
32
22
 
33
23
  def theme
34
24
  {
35
- container: "tab-content",
36
- input: tokens(
25
+ container: tokens(
37
26
  "tab",
38
27
  disabled: "tab-disabled",
39
28
  active: "tab-active"
@@ -8,6 +8,7 @@ module Protos
8
8
  def view_template(&block)
9
9
  form(method: :dialog, class: css[:form]) do
10
10
  button(
11
+ aria_label: "close",
11
12
  autofocus: true,
12
13
  formmethod: :dialog,
13
14
  formnovalidate: true,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Protos
4
- VERSION = "0.4.3"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/protos.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "protos/types"
15
15
  require_relative "protos/token_list"
16
16
  require_relative "protos/component"
17
17
  require_relative "protos/theme"
18
+ require_relative "protos/mix"
18
19
  require_relative "protos/attributes"
19
20
 
20
21
  # Components
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nolan J Tait
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-15 00:00:00.000000000 Z
11
+ date: 2024-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-core
@@ -149,6 +149,7 @@ files:
149
149
  - lib/protos/hero/overlay.rb
150
150
  - lib/protos/list.rb
151
151
  - lib/protos/list/item.rb
152
+ - lib/protos/mix.rb
152
153
  - lib/protos/modal.rb
153
154
  - lib/protos/modal/close_button.rb
154
155
  - lib/protos/modal/dialog.rb
@@ -220,7 +221,7 @@ requirements:
220
221
  - tailwindcss
221
222
  - daisyui
222
223
  - protos-stimulus
223
- rubygems_version: 3.5.17
224
+ rubygems_version: 3.5.18
224
225
  signing_key:
225
226
  specification_version: 4
226
227
  summary: A UI component library built with phlex and daisyui