stimulus_plumbers_tailwind 0.4.0 → 0.4.2

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: f14f719e9c94e67c1b76669ea4a0691c7929e9a2b7df04fda692ce3ea430c4bc
4
- data.tar.gz: 161c3622b129cbcf3ee49bc304319c691cf09c8c754a01646bc608480b391029
3
+ metadata.gz: d7821a54d820ecd2c76e2ce5afd0bcd00ab4fb2f78f3e4246a256a378b3bd631
4
+ data.tar.gz: f67b19c723fe3ba0e22c5f7e94698575bf17e905d6ec4db9305b919d6ae4a471
5
5
  SHA512:
6
- metadata.gz: 606d07453d71bc70cd71560c47ee12a028fbe4ab5ef3150d52024d61eef561f263c12221c73ed8689f56119b8f5b155099bad4526435f3614550b23f2934628c
7
- data.tar.gz: c4d45c72794f1c0df3310e30f5a95cb3bc3c204c1298eca5c916f645fb530d1bfb4d5ee3ff24c828290ba6e6a75e5980e4d20853eae19b1692ee64007303d60f
6
+ metadata.gz: 959b1d2a0f740c5630d85cc824da0148e7c172e9052b6359f4c873ee5c24ac23afdddf6bc5d4cd76602373f5c3f7d90cd0649fe13185b5eeb9b802deed618a6e
7
+ data.tar.gz: 4a0dd9d2040676acd2671eda6b2840ebb320f6704ce93ed23dcdcbb008ab37bc9abdd30e132bb224d9f0a97aa069941bdd4ee76b650eeeb71d97b7b12fe14f25
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
4
4
 
5
+ ---
6
+ ## [0.4.2](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-tailwind/v0.4.0..stimulus-plumbers-tailwind/v0.4.2) - 2026-06-30
7
+
8
+ ### Bug Fixes
9
+
10
+ - form submit button style ([#127](https://github.com/ryancyq/stimulus-plumbers/issues/127)) - ([2bbc73f](https://github.com/ryancyq/stimulus-plumbers/commit/2bbc73f15b7ffa2d9d8f87991adead33fab8156e)) - Ryan Chang
11
+ - various component styles ([#128](https://github.com/ryancyq/stimulus-plumbers/issues/128)) - ([011cc69](https://github.com/ryancyq/stimulus-plumbers/commit/011cc69bb6df3770949564521cdf949493d028cc)) - Ryan Chang
12
+ - use generic icon in core gem then provide alias in tailwind gem ([#129](https://github.com/ryancyq/stimulus-plumbers/issues/129)) - ([5c87d0d](https://github.com/ryancyq/stimulus-plumbers/commit/5c87d0da0c012652a3afa5aa8345b58d03e6ce5b)) - Ryan Chang
13
+ - combobox time ([#130](https://github.com/ryancyq/stimulus-plumbers/issues/130)) - ([ff5d968](https://github.com/ryancyq/stimulus-plumbers/commit/ff5d968da34fc723b0112a791bd95ef43f2b1640)) - Ryan Chang
14
+ - extract img avatar building block ([#132](https://github.com/ryancyq/stimulus-plumbers/issues/132)) - ([c3808b8](https://github.com/ryancyq/stimulus-plumbers/commit/c3808b8df38eecef940b51dab29f50bc815b4e74)) - Ryan Chang
15
+ - update floating style to label/input/group ([#135](https://github.com/ryancyq/stimulus-plumbers/issues/135)) - ([3947f38](https://github.com/ryancyq/stimulus-plumbers/commit/3947f38467e0a498fe51101d1859a573db93349f)) - Ryan Chang
16
+ - accessibility + plumber helpers ([#136](https://github.com/ryancyq/stimulus-plumbers/issues/136)) - ([e84053d](https://github.com/ryancyq/stimulus-plumbers/commit/e84053d43db4726c3704d459a0464f70529fff8b)) - Ryan Chang
17
+ - calendar disabled + out of range ([#148](https://github.com/ryancyq/stimulus-plumbers/issues/148)) - ([7bd1ca2](https://github.com/ryancyq/stimulus-plumbers/commit/7bd1ca21af1b982cf7df9460e0ba1c9e78866a87)) - Ryan Chang
18
+
19
+ ### Dependencies
20
+
21
+ - **(deps-dev)** bump @playwright/test in /stimulus-plumbers-tailwind ([#147](https://github.com/ryancyq/stimulus-plumbers/issues/147)) - ([941634d](https://github.com/ryancyq/stimulus-plumbers/commit/941634de18e2accc39f926258cb3d1b2534d11dc)) - dependabot[bot]
22
+ - **(deps-dev)** bump prettier in /stimulus-plumbers-tailwind ([#146](https://github.com/ryancyq/stimulus-plumbers/issues/146)) - ([570c8ff](https://github.com/ryancyq/stimulus-plumbers/commit/570c8ff7630039bdbdc6e3d89c6a181bbc706dd8)) - dependabot[bot]
23
+
24
+ ### Documentation
25
+
26
+ - update readme and component doc ([#134](https://github.com/ryancyq/stimulus-plumbers/issues/134)) - ([3e5f462](https://github.com/ryancyq/stimulus-plumbers/commit/3e5f4621c4fbf142af061659cfbb8cd3e99fd093)) - Ryan Chang
27
+ - update component documentation - ([89ecb78](https://github.com/ryancyq/stimulus-plumbers/commit/89ecb786d88eed66ab925921ac182aa84560207d)) - Ryan Chang
28
+
29
+ ### Features
30
+
31
+ - calendar redesign ([#133](https://github.com/ryancyq/stimulus-plumbers/issues/133)) - ([e19df95](https://github.com/ryancyq/stimulus-plumbers/commit/e19df95f3814b3d947142722ff0295f1bdcef6b3)) - Ryan Chang
32
+
33
+ ### Tests
34
+
35
+ - update sandbox view and spec coverage ([#122](https://github.com/ryancyq/stimulus-plumbers/issues/122)) - ([0fd3381](https://github.com/ryancyq/stimulus-plumbers/commit/0fd33817325070c29ffed82d37843b4c4be3e911)) - Ryan Chang
36
+ - update collection radio locator - ([f0fa9e8](https://github.com/ryancyq/stimulus-plumbers/commit/f0fa9e85bc0b835b3adc46f5dc406a8bdedbeefe)) - Ryan Chang
37
+
5
38
  ---
6
39
  ## [0.4.0](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-tailwind/v0.3.3..stimulus-plumbers-tailwind/v0.4.0) - 2026-06-14
7
40
 
data/README.md CHANGED
@@ -27,7 +27,7 @@ Activate the theme in an initializer or `config/application.rb`:
27
27
 
28
28
  ```ruby
29
29
  StimulusPlumbers.configure do |config|
30
- config.theme = :tailwind
30
+ config.theme.use(:tailwind)
31
31
  end
32
32
  ```
33
33
 
@@ -46,7 +46,6 @@ Use `bundle show stimulus_plumbers_tailwind` to get the exact installed path.
46
46
 
47
47
  | Module | Components |
48
48
  |--------|------------|
49
- | `Tailwind::ActionList` | Action list, menu items |
50
49
  | `Tailwind::Avatar` | Avatar |
51
50
  | `Tailwind::Button` | Button |
52
51
  | `Tailwind::Calendar` | Calendar grid, date picker |
@@ -55,6 +54,9 @@ Use `bundle show stimulus_plumbers_tailwind` to get the exact installed path.
55
54
  | `Tailwind::Form` | Form fields, labels, errors |
56
55
  | `Tailwind::Icon` | Icon (SVG rendering, icon registry) |
57
56
  | `Tailwind::Layout` | Layout primitives |
57
+ | `Tailwind::Link` | Link |
58
+ | `Tailwind::List` | List |
59
+ | `Tailwind::Timeline` | Timeline |
58
60
 
59
61
  Custom themes can subclass `TailwindTheme` to override individual methods, or subclass `StimulusPlumbers::Themes::Base` directly.
60
62
 
@@ -85,8 +87,8 @@ bundle install
85
87
  npm install
86
88
 
87
89
  bundle exec rake test:unit # unit tests
88
- npm run test:snapshots # visual snapshot tests (Playwright)
89
- npm run test:snapshots:update # regenerate baseline screenshots
90
+ node --run test:snapshots # visual snapshot tests (Playwright)
91
+ node --run test:snapshots:update # regenerate baseline screenshots
90
92
  bundle exec rake rubocop # lint
91
93
  bundle exec rake coverage # run tests with coverage + collate report
92
94
  ```
@@ -4,6 +4,18 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module Avatar
7
+ BASE = %w[
8
+ rounded-(--sp-radius-full)
9
+ overflow-hidden
10
+ inline-flex items-center justify-center
11
+ ].freeze
12
+
13
+ SIZES = {
14
+ sm: "size-(--sp-avatar-size-sm)",
15
+ md: "size-(--sp-avatar-size-md)",
16
+ lg: "size-(--sp-avatar-size-lg)"
17
+ }.freeze
18
+
7
19
  VARIANTS = {
8
20
  amber: "text-white bg-amber-600",
9
21
  lime: "text-white bg-lime-600",
@@ -19,18 +31,6 @@ module StimulusPlumbers
19
31
  blue: "text-white bg-blue-600"
20
32
  }.freeze
21
33
 
22
- SIZES = {
23
- sm: "size-(--sp-icon-size)",
24
- md: "size-(--sp-avatar-size)",
25
- lg: "size-(--sp-avatar-size-lg)"
26
- }.freeze
27
-
28
- BASE = %w[
29
- rounded-(--sp-radius-full)
30
- overflow-hidden
31
- inline-flex items-center justify-center
32
- ].freeze
33
-
34
34
  def avatar_variants
35
35
  VARIANTS
36
36
  end
@@ -4,6 +4,25 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module Button
7
+ # ── Structure ─────────────────────────────────────────────────────────
8
+ BASE = [
9
+ *Control::BASE,
10
+ "whitespace-nowrap",
11
+ "focus-visible:ring-(--btn-ring)"
12
+ ].freeze
13
+
14
+ LAYOUT = %w[
15
+ inline-flex items-center justify-center gap-(--sp-space-2)
16
+ [&:not(:has(>span))]:aspect-square
17
+ [&:not(:has(>span))]:px-0
18
+ ].freeze
19
+
20
+ CARD = %w[
21
+ inline-flex justify-start items-center flex-1 gap-(--sp-space-3) p-(--sp-space-4)
22
+ [&>:last-child:not(:first-child)]:ml-auto
23
+ ].freeze
24
+
25
+ # ── Color tokens ──────────────────────────────────────────────────────
7
26
  VARIANTS = {
8
27
  primary: %w[
9
28
  [--btn-bg:var(--sp-color-primary)]
@@ -56,6 +75,7 @@ module StimulusPlumbers
56
75
  ].freeze
57
76
  }.freeze
58
77
 
78
+ # ── Type styles ───────────────────────────────────────────────────────
59
79
  TYPES = {
60
80
  default: %w[
61
81
  rounded-(--sp-radius-md)
@@ -67,13 +87,13 @@ module StimulusPlumbers
67
87
  rounded-(--sp-radius-md)
68
88
  bg-(--sp-color-bg) text-(--btn-accent)
69
89
  border border-(--btn-border)
70
- hover:bg-(--btn-bg)/10
90
+ hover:bg-(--btn-accent)/10
71
91
  ].freeze,
72
92
  ghost: %w[
73
93
  rounded-(--sp-radius-md)
74
94
  bg-transparent text-(--btn-accent)
75
95
  border border-transparent
76
- hover:bg-(--btn-bg)/10
96
+ hover:bg-(--btn-accent)/10
77
97
  ].freeze,
78
98
  fab: %w[
79
99
  rounded-(--sp-radius-full)
@@ -93,16 +113,17 @@ module StimulusPlumbers
93
113
  rounded-(--sp-radius-md)
94
114
  bg-(--sp-color-bg) text-(--btn-accent)
95
115
  border border-dashed border-(--btn-border)
96
- hover:bg-(--btn-bg)/10
116
+ hover:bg-(--btn-accent)/10
97
117
  ].freeze,
98
118
  card: %w[
99
119
  rounded-(--sp-radius-md)
100
120
  bg-(--sp-color-bg) text-(--btn-accent)
101
121
  border border-(--btn-border) shadow-(--sp-shadow-xs)
102
- hover:bg-(--btn-bg)/10
122
+ hover:bg-(--btn-accent)/10
103
123
  ].freeze
104
124
  }.freeze
105
125
 
126
+ # ── Sizes ─────────────────────────────────────────────────────────────
106
127
  SIZES = {
107
128
  xs: %w[h-7 px-(--sp-space-2) text-(length:--sp-text-xs)].freeze,
108
129
  sm: %w[h-8 px-(--sp-space-3) text-(length:--sp-text-sm)].freeze,
@@ -111,23 +132,6 @@ module StimulusPlumbers
111
132
  xl: %w[h-14 px-(--sp-space-6) text-(length:--sp-text-lg)].freeze
112
133
  }.freeze
113
134
 
114
- BASE = [
115
- *Control::BASE,
116
- "whitespace-nowrap",
117
- "focus-visible:ring-(--btn-ring)"
118
- ].freeze
119
-
120
- LAYOUT = %w[
121
- inline-flex items-center justify-center gap-(--sp-space-2)
122
- [&:not(:has(>span))]:aspect-square
123
- [&:not(:has(>span))]:px-0
124
- ].freeze
125
-
126
- CARD = %w[
127
- inline-flex justify-start items-center flex-1 gap-(--sp-space-3) p-(--sp-space-4)
128
- [&>:last-child:not(:first-child)]:ml-auto
129
- ].freeze
130
-
131
135
  private
132
136
 
133
137
  def button_classes(type: :default, variant: :primary, size: :md)
@@ -143,7 +147,7 @@ module StimulusPlumbers
143
147
  end
144
148
 
145
149
  def button_icon_classes
146
- { classes: klasses("size-(--sp-control-size)", "stroke-current") }
150
+ { classes: klasses("size-(--sp-icon-size-sm)", "stroke-current") }
147
151
  end
148
152
  end
149
153
  end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module Calendar
7
- GRID = %w[w-full].freeze
7
+ GRID = [].freeze
8
8
 
9
9
  DAYS_OF_WEEK = %w[
10
10
  grid grid-cols-7 text-center text-(length:--sp-text-xs)
@@ -16,41 +16,52 @@ module StimulusPlumbers
16
16
  DAY = %w[
17
17
  size-(--sp-calendar-day-size) rounded-(--sp-radius-md)
18
18
  flex items-center justify-center text-(length:--sp-text-sm)
19
- hover:bg-(--sp-color-muted) cursor-pointer
19
+ [&:is(button)]:hover:bg-(--sp-color-muted) [&:is(button)]:cursor-pointer
20
20
  aria-[current=date]:font-bold
21
21
  aria-selected:bg-(--sp-color-primary)
22
22
  aria-selected:text-(--sp-color-primary-fg)
23
23
  aria-selected:hover:bg-(--sp-color-primary)/90
24
24
  aria-[hidden=true]:pointer-events-none
25
- aria-[hidden=true]:hover:bg-transparent
25
+ aria-[disabled=true]:text-(--sp-color-muted-fg)
26
+ aria-[disabled=true]:opacity-40
27
+ aria-[disabled=true]:pointer-events-none
28
+ disabled:text-(--sp-color-muted-fg)
29
+ disabled:opacity-40
30
+ disabled:pointer-events-none
26
31
  ].freeze
27
32
 
28
- QUARTER_GRID = %w[grid grid-cols-4].freeze
33
+ QUARTER_GRID = %w[grid grid-cols-4 gap-(--sp-space-1)].freeze
29
34
 
30
35
  MONTHS_OF_YEAR = "contents"
31
36
 
32
37
  MONTH = %w[
33
38
  rounded-(--sp-radius-md) flex items-center justify-center
34
- text-(length:--sp-text-sm) h-10 flex-1
39
+ text-(length:--sp-text-sm) h-10 flex-1 px-(--sp-space-2)
35
40
  hover:bg-(--sp-color-muted) cursor-pointer
36
41
  aria-selected:bg-(--sp-color-primary)
37
42
  aria-selected:text-(--sp-color-primary-fg)
38
43
  aria-selected:hover:bg-(--sp-color-primary)/90
39
- aria-disabled:pointer-events-none aria-disabled:text-(--sp-color-disabled-fg)
40
44
  aria-[current=month]:font-bold
45
+ aria-[disabled=true]:text-(--sp-color-muted-fg)
46
+ aria-[disabled=true]:opacity-40
47
+ aria-[disabled=true]:pointer-events-none
48
+ aria-[disabled=true]:hover:bg-transparent
41
49
  ].freeze
42
50
 
43
51
  YEARS_OF_DECADE = "contents"
44
52
 
45
53
  YEAR = %w[
46
54
  rounded-(--sp-radius-md) flex items-center justify-center
47
- text-(length:--sp-text-sm) h-10 flex-1
55
+ text-(length:--sp-text-sm) h-10 flex-1 px-(--sp-space-2)
48
56
  hover:bg-(--sp-color-muted) cursor-pointer
49
57
  aria-selected:bg-(--sp-color-primary)
50
58
  aria-selected:text-(--sp-color-primary-fg)
51
59
  aria-selected:hover:bg-(--sp-color-primary)/90
52
- aria-disabled:pointer-events-none aria-disabled:text-(--sp-color-disabled-fg)
53
60
  aria-[current=year]:font-bold
61
+ aria-[disabled=true]:text-(--sp-color-muted-fg)
62
+ aria-[disabled=true]:opacity-40
63
+ aria-[disabled=true]:pointer-events-none
64
+ aria-[disabled=true]:hover:bg-transparent
54
65
  ].freeze
55
66
 
56
67
  private
@@ -80,21 +91,13 @@ module StimulusPlumbers
80
91
  end
81
92
 
82
93
  def calendar_day_classes(outside: false, **)
83
- {
84
- classes: klasses(
85
- *DAY,
86
- *(outside ? %w[text-(--sp-color-disabled-fg)] : [])
87
- )
88
- }
94
+ extra = outside ? %w[text-(--sp-color-muted-fg)] : []
95
+ { classes: klasses(*DAY, *extra) }
89
96
  end
90
97
 
91
- def calendar_month_classes(**)
92
- { classes: klasses(*MONTH) }
93
- end
98
+ def calendar_month_classes(**) = { classes: klasses(*MONTH) }
94
99
 
95
- def calendar_year_classes(**)
96
- { classes: klasses(*YEAR) }
97
- end
100
+ def calendar_year_classes(**) = { classes: klasses(*YEAR) }
98
101
 
99
102
  def calendar_quarter_grid_classes
100
103
  { classes: klasses(*QUARTER_GRID) }
@@ -5,6 +5,7 @@ module StimulusPlumbers
5
5
  module Tailwind
6
6
  module Card
7
7
  BASE = %w[
8
+ flex flex-col
8
9
  rounded-(--sp-radius-md) border border-(--card-ring)
9
10
  bg-(--sp-color-bg) shadow-(--sp-shadow-xs)
10
11
  ].freeze
@@ -21,7 +22,7 @@ module StimulusPlumbers
21
22
 
22
23
  HEADER_BASE = %w[
23
24
  flex items-center gap-(--sp-space-3)
24
- px-(--sp-space-6) pt-(--sp-space-6)
25
+ px-(--sp-space-6) py-(--sp-space-6)
25
26
  ].freeze
26
27
 
27
28
  ICON_BASE = %w[
@@ -33,12 +34,10 @@ module StimulusPlumbers
33
34
  ].freeze
34
35
 
35
36
  BODY_BASE = %w[
36
- px-(--sp-space-6) pt-(--sp-space-3)
37
+ px-(--sp-space-6) py-(--sp-space-3)
37
38
  ].freeze
38
39
 
39
- ACTION_BASE = %w[
40
- px-(--sp-space-6) pb-(--sp-space-6) pt-(--sp-space-4)
41
- ].freeze
40
+ ACTION_BASE = %w[w-full justify-start].freeze
42
41
 
43
42
  private
44
43
 
@@ -4,8 +4,13 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module Combobox
7
+ # ── Container / popover ───────────────────────────────────────────────
8
+ CONTAINER = %w[relative].freeze
9
+ POPOVER = %w[absolute top-full left-0 min-w-full].freeze
10
+
11
+ # ── Trigger ───────────────────────────────────────────────────────────
7
12
  TRIGGER = %w[
8
- w-full rounded-(--sp-radius-md) border border-(--sp-color-muted-fg)
13
+ w-full rounded-(--sp-radius-md) border border-(--sp-color-muted-fg) hover:border-(--sp-color-fg)
9
14
  px-(--sp-space-3) py-(--sp-space-2)
10
15
  text-(length:--sp-text-sm) text-(--sp-color-fg) bg-(--sp-color-bg)
11
16
  focus:outline-none focus:ring-2 focus:ring-(--sp-focus-ring-color)
@@ -13,7 +18,7 @@ module StimulusPlumbers
13
18
 
14
19
  TRIGGER_GROUP = %w[
15
20
  flex items-center gap-(--sp-space-2) overflow-hidden
16
- rounded-(--sp-radius-md) border border-(--sp-color-muted-fg) bg-(--sp-color-bg)
21
+ rounded-(--sp-radius-md) border border-(--sp-color-muted-fg) hover:border-(--sp-color-fg) bg-(--sp-color-bg)
17
22
  px-(--sp-space-3) py-(--sp-space-2)
18
23
  focus-within:outline-none focus-within:ring-2 focus-within:ring-(--sp-focus-ring-color)
19
24
  [&>input]:border-0 [&>input]:rounded-none
@@ -22,8 +27,9 @@ module StimulusPlumbers
22
27
  [&>input]:focus:ring-0
23
28
  ].freeze
24
29
 
30
+ # ── Listbox / options ─────────────────────────────────────────────────
25
31
  LISTBOX = %w[
26
- py-(--sp-space-1) overflow-y-auto max-h-60
32
+ py-(--sp-space-1) overflow-y-auto max-h-60 min-h-(--sp-space-6)
27
33
  ].freeze
28
34
 
29
35
  OPTION_BASE = %w[
@@ -44,6 +50,7 @@ module StimulusPlumbers
44
50
 
45
51
  OPTION_GROUP = %w[py-(--sp-space-1)].freeze
46
52
 
53
+ # ── Typeahead states ──────────────────────────────────────────────────
47
54
  TYPEAHEAD_LOADING = %w[
48
55
  flex items-center justify-center
49
56
  py-(--sp-space-2) text-(length:--sp-text-sm) text-(--sp-color-muted-fg)
@@ -54,20 +61,15 @@ module StimulusPlumbers
54
61
  py-(--sp-space-2) text-(length:--sp-text-sm) text-(--sp-color-muted-fg)
55
62
  ].freeze
56
63
 
57
- TIME = %w[flex gap-(--sp-space-2) overflow-hidden].freeze
58
-
59
- DATE_NAV = %w[flex items-center justify-between gap-(--sp-space-1) mb-(--sp-space-2)].freeze
60
-
61
- DATE_NAV_BTN = [
62
- *Control::BASE,
63
- "inline-flex items-center justify-center",
64
- "size-(--sp-calendar-day-size) rounded-(--sp-radius-md)",
65
- "text-(--sp-color-fg) hover:bg-(--sp-color-muted)",
66
- "focus-visible:ring-(--sp-focus-ring-color)"
67
- ].freeze
64
+ # ── Time picker ───────────────────────────────────────────────────────
65
+ TIME = %w[flex gap-(--sp-space-2) overflow-hidden].freeze
66
+ TIME_DRUM_UNIT = %w[flex-1 min-w-0].freeze
67
+ TIME_DRUM_PERIOD = %w[shrink-0].freeze
68
68
 
69
- CONTAINER = %w[relative].freeze
70
- POPOVER = %w[absolute top-full left-0 min-w-full].freeze
69
+ # ── Date navigation ───────────────────────────────────────────────────
70
+ DATE_NAV = %w[flex items-center justify-between gap-(--sp-space-1) mb-(--sp-space-2)].freeze
71
+ DATE_NAV_BTN = %w[].freeze
72
+ DATE_NAV_TITLE = %w[flex-1 text-center].freeze
71
73
 
72
74
  private
73
75
 
@@ -110,7 +112,7 @@ module StimulusPlumbers
110
112
  end
111
113
 
112
114
  def combobox_typeahead_loading_icon_classes
113
- { classes: klasses("size-(--sp-icon-size)", "animate-spin") }
115
+ { classes: klasses("size-(--sp-icon-size-md)", "animate-spin") }
114
116
  end
115
117
 
116
118
  def combobox_typeahead_empty_classes
@@ -121,6 +123,10 @@ module StimulusPlumbers
121
123
  { classes: klasses(*TIME) }
122
124
  end
123
125
 
126
+ def combobox_time_drum_classes(type: :unit)
127
+ { classes: klasses(*(type == :period ? TIME_DRUM_PERIOD : TIME_DRUM_UNIT)) }
128
+ end
129
+
124
130
  def combobox_date_navigation_classes
125
131
  { classes: klasses(*DATE_NAV) }
126
132
  end
@@ -128,6 +134,10 @@ module StimulusPlumbers
128
134
  def combobox_date_navigation_navigator_classes
129
135
  { classes: klasses(*DATE_NAV_BTN) }
130
136
  end
137
+
138
+ def combobox_date_navigation_title_classes
139
+ { classes: klasses(*DATE_NAV_TITLE) }
140
+ end
131
141
  end
132
142
  end
133
143
  end
@@ -10,27 +10,6 @@ module StimulusPlumbers
10
10
  HINT = %w[text-(length:--sp-text-xs) text-(--sp-color-muted-fg)].freeze
11
11
  ERROR_TEXT = %w[text-(length:--sp-text-xs) text-(--sp-color-error)].freeze
12
12
 
13
- FLOATING_INPUT_BASE = %w[
14
- peer w-full text-(length:--sp-text-sm) text-(--sp-color-fg) appearance-none
15
- focus:outline-none focus:ring-0
16
- ].freeze
17
- FLOATING_INPUT_TYPES = {
18
- filled: %w[
19
- rounded-t-(--sp-radius-md) px-(--sp-space-2-5) pb-(--sp-space-2-5) pt-(--sp-space-5)
20
- bg-(--sp-color-bg-muted) border-0 border-b-2
21
- ].freeze,
22
- outlined: %w[
23
- px-(--sp-space-2-5) pb-(--sp-space-2-5) pt-(--sp-space-4)
24
- bg-transparent rounded-(--sp-radius-md) border
25
- ].freeze,
26
- standard: %w[
27
- py-(--sp-space-2-5) px-0
28
- bg-transparent border-0 border-b-2
29
- ].freeze
30
- }.freeze
31
- FLOATING_INPUT_ERROR = %w[border-(--sp-color-error)].freeze
32
- FLOATING_INPUT_DEFAULT = %w[border-(--sp-color-muted-fg) focus:border-(--sp-color-primary)].freeze
33
-
34
13
  FLOATING_GROUP_TYPES = {
35
14
  filled: %w[relative].freeze,
36
15
  outlined: %w[relative].freeze,
@@ -41,28 +20,42 @@ module StimulusPlumbers
41
20
  absolute text-(length:--sp-text-sm) text-(--sp-color-muted-fg)
42
21
  duration-300 transform origin-[0]
43
22
  ].freeze
44
- FLOATING_LABEL_FOCUS = %w[peer-focus:text-(--sp-color-primary)].freeze
23
+ FLOATING_LABEL_FOCUS = %w[peer-focus-within:text-(--sp-color-primary)].freeze
45
24
  FLOATING_LABEL_ERROR = %w[text-(--sp-color-error)].freeze
46
25
  FLOATING_LABEL_TYPES = {
47
26
  filled: %w[
48
27
  -translate-y-(--sp-space-4) scale-75 top-(--sp-space-4) z-10 start-(--sp-space-2-5)
49
- peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0
50
- peer-focus:scale-75 peer-focus:-translate-y-(--sp-space-4)
51
- rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto
28
+ peer-placeholder-shown:scale-100
29
+ peer-placeholder-shown:translate-y-0
30
+ peer-has-[input:not(:focus):placeholder-shown]:scale-100
31
+ peer-has-[input:not(:focus):placeholder-shown]:translate-y-0
32
+ peer-focus-within:scale-75
33
+ peer-focus-within:-translate-y-(--sp-space-4)
34
+ rtl:peer-focus-within:translate-x-1/4 rtl:peer-focus-within:left-auto
52
35
  ].freeze,
53
36
  outlined: %w[
54
37
  -translate-y-(--sp-space-4) scale-75 top-(--sp-space-2) z-10 start-1
55
- bg-(--sp-color-bg) px-(--sp-space-2) peer-focus:px-(--sp-space-2)
56
- peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2
57
- peer-focus:top-(--sp-space-2) peer-focus:scale-75 peer-focus:-translate-y-(--sp-space-4)
58
- rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto
38
+ bg-(--sp-color-bg) px-(--sp-space-2) peer-focus-within:px-(--sp-space-2)
39
+ peer-placeholder-shown:scale-100
40
+ peer-placeholder-shown:-translate-y-1/2
41
+ peer-placeholder-shown:top-1/2
42
+ peer-has-[input:not(:focus):placeholder-shown]:scale-100
43
+ peer-has-[input:not(:focus):placeholder-shown]:-translate-y-1/2
44
+ peer-has-[input:not(:focus):placeholder-shown]:top-1/2
45
+ peer-focus-within:top-(--sp-space-2) peer-focus-within:scale-75
46
+ peer-focus-within:-translate-y-(--sp-space-4)
47
+ rtl:peer-focus-within:translate-x-1/4 rtl:peer-focus-within:left-auto
59
48
  ].freeze,
60
49
  standard: %w[
61
50
  -translate-y-(--sp-space-6) scale-75 top-(--sp-space-3) -z-10 start-0
62
- peer-focus:start-0
63
- peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0
64
- peer-focus:scale-75 peer-focus:-translate-y-(--sp-space-6)
65
- rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto
51
+ peer-focus-within:start-0
52
+ peer-placeholder-shown:scale-100
53
+ peer-placeholder-shown:translate-y-0
54
+ peer-has-[input:not(:focus):placeholder-shown]:scale-100
55
+ peer-has-[input:not(:focus):placeholder-shown]:translate-y-0
56
+ peer-focus-within:scale-75
57
+ peer-focus-within:-translate-y-(--sp-space-6)
58
+ rtl:peer-focus-within:translate-x-1/4 rtl:peer-focus-within:left-auto
66
59
  ].freeze
67
60
  }.freeze
68
61
 
@@ -78,7 +71,7 @@ module StimulusPlumbers
78
71
  hover:bg-(--sp-color-muted)
79
72
  ].freeze,
80
73
  card: %w[
81
- flex justify-between items-start flex-1 p-(--sp-space-4) cursor-pointer select-none
74
+ flex justify-between items-center gap-(--sp-space-3) flex-1 p-(--sp-space-4) cursor-pointer select-none
82
75
  text-(length:--sp-text-sm) text-(--sp-color-muted-fg)
83
76
  bg-(--sp-color-bg) border border-(--sp-color-border) rounded-(--sp-radius-md) shadow-(--sp-shadow-xs)
84
77
  hover:bg-(--sp-color-muted) hover:border-(--sp-color-border-strong) hover:text-(--sp-color-fg)
@@ -97,16 +90,16 @@ module StimulusPlumbers
97
90
  text-(length:--sp-text-sm) text-(--sp-color-muted-fg)
98
91
  bg-(--sp-color-bg) border border-(--sp-color-border) rounded-(--sp-radius-md)
99
92
  hover:bg-(--sp-color-muted)
100
- peer-checked:border-(--card-ring) peer-checked:bg-(--card-ring)/10
101
- peer-checked:text-(--sp-color-fg) peer-checked:hover:bg-(--card-ring)/15
93
+ group-has-[:checked]:border-(--card-ring) group-has-[:checked]:bg-(--card-ring)/10
94
+ group-has-[:checked]:text-(--sp-color-fg) group-has-[:checked]:hover:bg-(--card-ring)/15
102
95
  ].freeze,
103
96
  card: %w[
104
97
  flex items-start flex-1 p-(--sp-space-4) cursor-pointer select-none
105
98
  text-(length:--sp-text-sm) text-(--sp-color-muted-fg)
106
99
  bg-(--sp-color-bg) border border-(--sp-color-border) rounded-(--sp-radius-md) shadow-(--sp-shadow-xs)
107
100
  hover:bg-(--sp-color-muted) hover:border-(--sp-color-border-strong) hover:text-(--sp-color-fg)
108
- peer-checked:border-(--card-ring) peer-checked:bg-(--card-ring)/10
109
- peer-checked:text-(--sp-color-fg) peer-checked:hover:bg-(--card-ring)/15
101
+ group-has-[:checked]:border-(--card-ring) group-has-[:checked]:bg-(--card-ring)/10
102
+ group-has-[:checked]:text-(--sp-color-fg) group-has-[:checked]:hover:bg-(--card-ring)/15
110
103
  ].freeze
111
104
  }.freeze
112
105
 
@@ -117,27 +110,17 @@ module StimulusPlumbers
117
110
 
118
111
  private
119
112
 
120
- def form_field_floating_classes(type: nil, error: false)
121
- {
122
- classes: klasses(
123
- *FLOATING_INPUT_BASE,
124
- *FLOATING_INPUT_TYPES.fetch(type, []),
125
- *(error ? FLOATING_INPUT_ERROR : FLOATING_INPUT_DEFAULT)
126
- )
127
- }
128
- end
129
-
130
- def form_field_floating_group_classes(type: nil)
131
- { classes: klasses(*FLOATING_GROUP_TYPES.fetch(type, [])) }
113
+ def form_field_input_group_classes(floating: nil)
114
+ { classes: klasses(*FLOATING_GROUP_TYPES.fetch(floating, [])) }
132
115
  end
133
116
 
134
- def form_field_floating_label_classes(type: nil, error: false)
135
- color = error ? FLOATING_LABEL_ERROR : FLOATING_LABEL_FOCUS
136
- { classes: klasses(*FLOATING_LABEL_BASE, *FLOATING_LABEL_TYPES.fetch(type, []), *color) }
137
- end
138
-
139
- def form_field_label_classes(hidden: false, **)
140
- { classes: klasses(*LABEL, hidden ? "sr-only" : nil) }
117
+ def form_field_label_classes(floating: nil, hidden: false, error: false, **)
118
+ if floating
119
+ color = error ? FLOATING_LABEL_ERROR : FLOATING_LABEL_FOCUS
120
+ { classes: klasses(*FLOATING_LABEL_BASE, *FLOATING_LABEL_TYPES.fetch(floating, []), *color) }
121
+ else
122
+ { classes: klasses(*LABEL, hidden ? "sr-only" : nil) }
123
+ end
141
124
  end
142
125
 
143
126
  def form_field_required_mark_classes
@@ -165,6 +148,10 @@ module StimulusPlumbers
165
148
  card_color = %i[button card].include?(type) ? Card::VARIANTS.fetch(variant, Card::VARIANTS[:tertiary]) : []
166
149
  { classes: klasses(*RADIO_LABEL_TYPES.fetch(type), *card_color) }
167
150
  end
151
+
152
+ def form_field_radio_item_group_classes
153
+ { classes: "contents group" }
154
+ end
168
155
  end
169
156
  end
170
157
  end
@@ -5,31 +5,98 @@ module StimulusPlumbers
5
5
  module Tailwind
6
6
  module Form
7
7
  module Input
8
+ # ── Standalone input ──────────────────────────────────────────────────
8
9
  INPUT_BASE = %w[
9
- w-full rounded-(--sp-radius-md) border px-(--sp-space-3) py-(--sp-space-2) text-(length:--sp-text-sm)
10
- text-(--sp-color-fg) bg-(--sp-color-bg) focus:outline-none focus:ring-2 focus:ring-offset-0
10
+ w-full rounded-(--sp-radius-md) border px-(--sp-space-3) py-(--sp-space-2)
11
+ text-(length:--sp-text-sm) text-(--sp-color-fg) bg-(--sp-color-bg)
12
+ focus:outline-none focus:ring-(length:--sp-focus-ring-width) focus:ring-offset-0
13
+ ].freeze
14
+ INPUT_DEFAULT = %w[
15
+ border-(--sp-color-muted-fg) hover:border-(--sp-color-fg)
16
+ focus:ring-(--sp-focus-ring-color)
17
+ ].freeze
18
+ INPUT_ERROR = %w[border-(--sp-color-error) focus:ring-(--sp-color-error)].freeze
19
+
20
+ # ── Floating input ────────────────────────────────────────────────────
21
+ FLOATING_INPUT_BASE = %w[
22
+ peer w-full text-(length:--sp-text-sm) text-(--sp-color-fg) appearance-none
23
+ focus:outline-none focus:ring-0
24
+ focus-visible:outline-none focus-visible:ring-0
25
+ ].freeze
26
+ FLOATING_INPUT_TYPES = {
27
+ filled: %w[
28
+ rounded-t-(--sp-radius-md) px-(--sp-space-2-5) pb-(--sp-space-2-5) pt-(--sp-space-5)
29
+ bg-(--sp-color-bg-muted) border-0 border-b-2
30
+ ].freeze,
31
+ outlined: %w[
32
+ px-(--sp-space-2-5) pb-(--sp-space-2-5) pt-(--sp-space-4)
33
+ bg-transparent rounded-(--sp-radius-md) border
34
+ ].freeze,
35
+ standard: %w[
36
+ py-(--sp-space-2-5) px-0
37
+ bg-transparent border-0 border-b-2
38
+ ].freeze
39
+ }.freeze
40
+ FLOATING_INPUT_DEFAULT = %w[
41
+ border-(--sp-color-muted-fg) hover:border-(--sp-color-fg)
42
+ focus:border-(--sp-color-primary)
43
+ ].freeze
44
+ FLOATING_INPUT_ERROR = %w[border-(--sp-color-error)].freeze
45
+
46
+ # ── Input group ───────────────────────────────────────────────────────
47
+ INPUT_GROUP_BASE = %w[flex items-center overflow-hidden rounded-(--sp-radius-md) border].freeze
48
+ INPUT_GROUP_BORDER = { error: "border-(--sp-color-error)", default: "border-(--sp-color-muted-fg)" }.freeze
49
+
50
+ # ── Floating input group ──────────────────────────────────────────────
51
+ FLOATING_INPUT_GROUP_BASE = %w[flex items-center overflow-hidden peer].freeze
52
+ FLOATING_INPUT_GROUP_TYPES = {
53
+ filled: %w[rounded-t-(--sp-radius-md) bg-(--sp-color-bg-muted) border-0 border-b-2].freeze,
54
+ outlined: %w[rounded-(--sp-radius-md) border].freeze,
55
+ standard: %w[rounded-none bg-transparent border-0 border-b-2].freeze
56
+ }.freeze
57
+ FLOATING_INPUT_GROUP_DEFAULT = %w[
58
+ border-(--sp-color-muted-fg) hover:border-(--sp-color-fg)
59
+ focus-within:border-(--sp-color-primary)
60
+ ].freeze
61
+ FLOATING_INPUT_GROUP_ERROR = %w[border-(--sp-color-error)].freeze
62
+
63
+ # ── Combobox wrappers ─────────────────────────────────────────────────
64
+ COMBOBOX_INPUT = %w[
65
+ [&>input:not([type=hidden])]:border-0
66
+ [&>input:not([type=hidden])]:rounded-none
67
+ [&>input:not([type=hidden])]:px-0
68
+ [&>input:not([type=hidden])]:py-0
69
+ [&>input:not([type=hidden])]:bg-transparent
70
+ [&>input:not([type=hidden])]:shadow-none
71
+ [&>input:not([type=hidden])]:focus:ring-0
72
+ ].freeze
73
+ COMBOBOX_TRIGGER_GROUP = %w[
74
+ [&>div:first-child]:border-0
75
+ [&>div:first-child]:rounded-none
76
+ [&>div:first-child]:px-0
77
+ [&>div:first-child]:py-0
78
+ [&>div:first-child]:focus-within:ring-0
11
79
  ].freeze
12
- INPUT_ERROR = %w[border-(--sp-color-error) focus:ring-(--sp-color-error)].freeze
13
- INPUT_DEFAULT = %w[border-(--sp-color-muted-fg) focus:ring-(--sp-focus-ring-color)].freeze
14
80
 
81
+ # ── Choice inputs ─────────────────────────────────────────────────────
15
82
  CHECKBOX_TYPES = {
16
83
  default: %w[
17
84
  size-(--sp-control-size) rounded-(--sp-radius-sm) shrink-0
18
85
  border border-(--sp-color-border) bg-(--sp-color-muted)
19
- focus:ring-2 focus:ring-(--sp-focus-ring-color) focus:outline-none
86
+ focus:ring-(length:--sp-focus-ring-width) focus:ring-(--sp-focus-ring-color) focus:outline-none
20
87
  disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer
21
88
  ].freeze,
22
89
  button: %w[
23
90
  size-(--sp-control-size) rounded-(--sp-radius-sm) shrink-0
24
91
  border border-(--sp-color-border) bg-(--sp-color-muted)
25
- focus:ring-2 focus:ring-(--sp-focus-ring-color) focus:outline-none
92
+ focus:ring-(length:--sp-focus-ring-width) focus:ring-(--sp-focus-ring-color) focus:outline-none
26
93
  disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer
27
94
  ].freeze,
28
95
  card: %w[
29
96
  size-(--sp-control-size) rounded-(--sp-radius-sm) shrink-0
30
97
  border border-(--sp-color-border) bg-(--sp-color-muted)
31
98
  checked:border-(--card-ring)
32
- focus:ring-2 focus:ring-(--card-ring) focus:outline-none
99
+ focus:ring-(length:--sp-focus-ring-width) focus:ring-(--card-ring) focus:outline-none
33
100
  disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer
34
101
  ].freeze
35
102
  }.freeze
@@ -38,58 +105,54 @@ module StimulusPlumbers
38
105
  default: %w[
39
106
  size-(--sp-control-size) rounded-full shrink-0
40
107
  [accent-color:var(--sp-color-primary)] cursor-pointer
41
- focus:ring-2 focus:ring-(--sp-focus-ring-color) focus:outline-none
108
+ focus:ring-(length:--sp-focus-ring-width) focus:ring-(--sp-focus-ring-color) focus:outline-none
42
109
  disabled:opacity-50 disabled:cursor-not-allowed
43
110
  ].freeze,
44
- button: %w[hidden peer].freeze,
45
- card: %w[hidden peer].freeze
111
+ button: %w[hidden].freeze,
112
+ card: %w[hidden].freeze
46
113
  }.freeze
47
114
 
48
- INPUT_GROUP_BASE = %w[flex items-center overflow-hidden rounded-(--sp-radius-md) border].freeze
49
- INPUT_GROUP_BORDER = { error: "border-(--sp-color-error)", default: "border-(--sp-color-muted-fg)" }.freeze
50
-
51
- COMBOBOX_INPUT = %w[
52
- [&>input:not([type=hidden])]:border-0
53
- [&>input:not([type=hidden])]:rounded-none
54
- [&>input:not([type=hidden])]:px-0
55
- [&>input:not([type=hidden])]:py-0
56
- [&>input:not([type=hidden])]:bg-transparent
57
- [&>input:not([type=hidden])]:shadow-none
58
- [&>input:not([type=hidden])]:focus:ring-0
59
- ].freeze
60
- COMBOBOX_TRIGGER_GROUP = %w[
61
- [&>div:first-child]:border-0
62
- [&>div:first-child]:rounded-none
63
- [&>div:first-child]:px-0
64
- [&>div:first-child]:py-0
65
- [&>div:first-child]:focus-within:ring-0
66
- ].freeze
67
-
68
- BUTTON_REVEAL = %w[
69
- self-stretch border-0 bg-transparent px-(--sp-space-3) cursor-pointer text-(--sp-color-muted-fg)
70
- hover:text-(--sp-color-fg) text-(length:--sp-text-sm)
115
+ # ── Utility buttons ───────────────────────────────────────────────────
116
+ BUTTON_REVEAL = [
117
+ *Control::BASE,
118
+ "inline-flex items-center justify-center",
119
+ "focus-visible:ring-(--sp-focus-ring-color)",
120
+ "self-stretch px-(--sp-space-2) border-0 bg-transparent cursor-pointer text-(--sp-color-muted-fg)",
121
+ "rounded-(--sp-radius-sm) hover:bg-(--sp-color-muted) hover:text-(--sp-color-fg)"
71
122
  ].freeze
72
- BUTTON_CLEAR = %w[
73
- self-stretch border-0 bg-transparent px-(--sp-space-2) cursor-pointer text-(--sp-color-muted-fg)
74
- hover:text-(--sp-color-fg) text-(length:--sp-text-sm)
123
+ BUTTON_CLEAR = [
124
+ *Control::BASE,
125
+ "inline-flex items-center justify-center",
126
+ "focus-visible:ring-(--sp-focus-ring-color)",
127
+ "self-stretch px-(--sp-space-2) border-0 bg-transparent cursor-pointer text-(--sp-color-muted-fg)",
128
+ "rounded-(--sp-radius-sm) hover:bg-(--sp-color-muted) hover:text-(--sp-color-fg)"
75
129
  ].freeze
76
130
 
77
131
  private
78
132
 
79
- def form_field_input_classes(error: false)
80
- { classes: klasses(*INPUT_BASE, *(error ? INPUT_ERROR : INPUT_DEFAULT)) }
133
+ def form_field_input_classes(floating: nil, error: false)
134
+ if floating
135
+ { classes: klasses(
136
+ *FLOATING_INPUT_BASE,
137
+ *FLOATING_INPUT_TYPES.fetch(floating, []),
138
+ *(error ? FLOATING_INPUT_ERROR : FLOATING_INPUT_DEFAULT)
139
+ )
140
+ }
141
+ else
142
+ { classes: klasses(*INPUT_BASE, *(error ? INPUT_ERROR : INPUT_DEFAULT)) }
143
+ end
81
144
  end
82
145
 
83
- def form_field_input_textarea_classes(error: false)
84
- form_field_input_classes(error: error)
146
+ def form_field_input_textarea_classes(floating: nil, error: false)
147
+ form_field_input_classes(floating: floating, error: error)
85
148
  end
86
149
 
87
- def form_field_input_file_classes(error: false)
88
- form_field_input_classes(error: error)
150
+ def form_field_input_file_classes(floating: nil, error: false)
151
+ form_field_input_classes(floating: floating, error: error)
89
152
  end
90
153
 
91
- def form_field_input_select_classes(error: false)
92
- form_field_input_classes(error: error)
154
+ def form_field_input_select_classes(floating: nil, error: false)
155
+ form_field_input_classes(floating: floating, error: error)
93
156
  end
94
157
 
95
158
  def form_field_input_checkbox_classes(type: :default, variant: :default, **)
@@ -102,23 +165,46 @@ module StimulusPlumbers
102
165
  { classes: klasses(*RADIO_TYPES.fetch(type), *card_color) }
103
166
  end
104
167
 
105
- def input_group_classes(error: false)
106
- { classes: klasses(*INPUT_GROUP_BASE, INPUT_GROUP_BORDER[error ? :error : :default]) }
168
+ def input_group_classes(error: false, floating: nil)
169
+ if floating
170
+ color = error ? FLOATING_INPUT_GROUP_ERROR : FLOATING_INPUT_GROUP_DEFAULT
171
+ { classes: klasses(*FLOATING_INPUT_GROUP_BASE, *FLOATING_INPUT_GROUP_TYPES.fetch(floating, []), *color) }
172
+ else
173
+ { classes: klasses(*INPUT_GROUP_BASE, INPUT_GROUP_BORDER[error ? :error : :default]) }
174
+ end
107
175
  end
108
176
 
109
- def form_field_input_combobox_classes(error: false)
110
- { classes: klasses(
111
- *INPUT_BASE,
112
- *(error ? INPUT_ERROR : INPUT_DEFAULT),
113
- *COMBOBOX_INPUT,
114
- *COMBOBOX_TRIGGER_GROUP
115
- )
116
- }
177
+ def form_field_input_combobox_classes(floating: nil, error: false)
178
+ if floating
179
+ form_field_input_combobox_floating_classes(floating: floating, error: error)
180
+ else
181
+ {
182
+ classes: klasses(
183
+ *INPUT_BASE,
184
+ *(error ? INPUT_ERROR : INPUT_DEFAULT),
185
+ *COMBOBOX_INPUT,
186
+ *COMBOBOX_TRIGGER_GROUP
187
+ )
188
+ }
189
+ end
190
+ end
191
+
192
+ def form_field_input_combobox_floating_classes(floating: :standard, error: false)
193
+ {
194
+ classes: klasses(
195
+ *FLOATING_INPUT_BASE,
196
+ *FLOATING_INPUT_TYPES.fetch(floating, []),
197
+ *(error ? FLOATING_INPUT_ERROR : FLOATING_INPUT_DEFAULT),
198
+ *COMBOBOX_INPUT,
199
+ *COMBOBOX_TRIGGER_GROUP
200
+ )
201
+ }
117
202
  end
118
203
 
119
204
  def form_field_input_reveal_classes(**)
120
205
  {
121
206
  classes: klasses(
207
+ "peer",
122
208
  "[&>input]:border-0",
123
209
  "[&>input]:rounded-none",
124
210
  "[&>input]:bg-transparent",
@@ -13,16 +13,7 @@ module StimulusPlumbers
13
13
  { classes: klasses(*GROUP_BASE, layout == :inline ? GROUP_INLINE : "flex-col") }
14
14
  end
15
15
 
16
- def form_submit_classes(type: :default, variant: :primary)
17
- { classes: klasses(
18
- *Button::BASE,
19
- *Button::LAYOUT,
20
- *Button::VARIANTS.fetch(variant, Button::VARIANTS[:primary]),
21
- *Button::TYPES.fetch(type, Button::TYPES[:default]),
22
- *(type == :card ? [] : Button::SIZES[:md])
23
- )
24
- }
25
- end
16
+ def form_submit_classes(**) = {}
26
17
  end
27
18
  end
28
19
  end
@@ -10,8 +10,13 @@ module StimulusPlumbers
10
10
  module Icon
11
11
  ALIASES = {
12
12
  "close" => "x-mark",
13
+ "download" => "arrow-down-tray",
14
+ "book" => "book-open",
15
+ "edit" => "pencil",
16
+ "email" => "envelope",
13
17
  "calendar" => "calendar-days",
14
- "external-link" => "arrow-top-right-on-square"
18
+ "external-link" => "arrow-top-right-on-square",
19
+ "reveal" => "eye"
15
20
  }.freeze
16
21
 
17
22
  ICONS = StimulusPlumbers::Themes::Icons::Registry.new(
@@ -26,7 +31,7 @@ module StimulusPlumbers
26
31
  private
27
32
 
28
33
  def icon_classes
29
- { classes: "size-(--sp-icon-default)" }
34
+ { classes: "size-(--sp-icon-size-lg)" }
30
35
  end
31
36
  end
32
37
  end
@@ -4,9 +4,12 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module Layout
7
- DIVIDER_SEPARATOR = %w[border-t border-(--sp-color-border) my-(--sp-space-1)].freeze
8
- DIVIDER = %w[flex items-center gap-(--sp-space-3)].freeze
9
- DIVIDER_LABEL = %w[text-(length:--sp-text-sm) text-(--sp-color-fg-muted) whitespace-nowrap font-medium].freeze
7
+ # ── Divider ───────────────────────────────────────────────────────────
8
+ DIVIDER = %w[w-full flex items-center gap-(--sp-space-3)].freeze
9
+ DIVIDER_SEPARATOR = %w[flex-1 h-px bg-(--sp-color-border) border-0].freeze
10
+ DIVIDER_LABEL = %w[text-(length:--sp-text-sm) text-(--sp-color-muted-fg) whitespace-nowrap font-medium].freeze
11
+
12
+ # ── Popover ───────────────────────────────────────────────────────────
10
13
  POPOVER_WRAPPER = %w[relative inline-block].freeze
11
14
  POPOVER_TRIGGER = [
12
15
  *Control::BASE,
@@ -4,34 +4,7 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module Link
7
- VARIANTS = {
8
- default: %w[
9
- [--link-color:var(--sp-color-primary)]
10
- [--link-ring:var(--sp-color-primary)]
11
- [--link-bg:var(--sp-color-primary)]
12
- ].freeze,
13
- success: %w[
14
- [--link-color:var(--sp-color-success)]
15
- [--link-ring:var(--sp-color-success-ring)]
16
- [--link-bg:var(--sp-color-success)]
17
- ].freeze,
18
- destructive: %w[
19
- [--link-color:var(--sp-color-destructive)]
20
- [--link-ring:var(--sp-color-destructive)]
21
- [--link-bg:var(--sp-color-destructive)]
22
- ].freeze,
23
- warning: %w[
24
- [--link-color:var(--sp-color-warning)]
25
- [--link-ring:var(--sp-color-warning-ring)]
26
- [--link-bg:var(--sp-color-warning)]
27
- ].freeze,
28
- info: %w[
29
- [--link-color:var(--sp-color-info)]
30
- [--link-ring:var(--sp-color-info-ring)]
31
- [--link-bg:var(--sp-color-info)]
32
- ].freeze
33
- }.freeze
34
-
7
+ # ── Type styles ───────────────────────────────────────────────────────
35
8
  BASE = [
36
9
  *Control::BASE,
37
10
  "inline-flex items-center gap-(--sp-space-1)",
@@ -65,6 +38,35 @@ module StimulusPlumbers
65
38
  "focus-visible:ring-(--link-ring)"
66
39
  ].freeze
67
40
 
41
+ # ── Color tokens ──────────────────────────────────────────────────────
42
+ VARIANTS = {
43
+ default: %w[
44
+ [--link-color:var(--sp-color-primary)]
45
+ [--link-ring:var(--sp-color-primary)]
46
+ [--link-bg:var(--sp-color-primary)]
47
+ ].freeze,
48
+ success: %w[
49
+ [--link-color:var(--sp-color-success)]
50
+ [--link-ring:var(--sp-color-success-ring)]
51
+ [--link-bg:var(--sp-color-success)]
52
+ ].freeze,
53
+ destructive: %w[
54
+ [--link-color:var(--sp-color-destructive)]
55
+ [--link-ring:var(--sp-color-destructive)]
56
+ [--link-bg:var(--sp-color-destructive)]
57
+ ].freeze,
58
+ warning: %w[
59
+ [--link-color:var(--sp-color-warning)]
60
+ [--link-ring:var(--sp-color-warning-ring)]
61
+ [--link-bg:var(--sp-color-warning)]
62
+ ].freeze,
63
+ info: %w[
64
+ [--link-color:var(--sp-color-info)]
65
+ [--link-ring:var(--sp-color-info-ring)]
66
+ [--link-bg:var(--sp-color-info)]
67
+ ].freeze
68
+ }.freeze
69
+
68
70
  private
69
71
 
70
72
  def link_classes(type: :default, variant: :default)
@@ -77,7 +79,7 @@ module StimulusPlumbers
77
79
  end
78
80
 
79
81
  def link_icon_classes
80
- { classes: klasses("size-(--sp-control-size)", "stroke-current") }
82
+ { classes: klasses("size-(--sp-icon-size-sm)", "stroke-current") }
81
83
  end
82
84
  end
83
85
  end
@@ -4,6 +4,19 @@ module StimulusPlumbers
4
4
  module Themes
5
5
  module Tailwind
6
6
  module List
7
+ # ── Section ───────────────────────────────────────────────────────────
8
+ SECTION_TITLE_BASE = %w[
9
+ block px-(--sp-space-2) pb-(--sp-space-1)
10
+ text-(length:--sp-text-xs) font-semibold uppercase tracking-wider
11
+ text-(--sp-color-muted-fg)
12
+ ].freeze
13
+
14
+ SECTION_DESCRIPTION_BASE = %w[
15
+ block px-(--sp-space-2) pb-(--sp-space-1)
16
+ text-(length:--sp-text-xs) text-(--sp-color-muted-fg)
17
+ ].freeze
18
+
19
+ # ── Item ──────────────────────────────────────────────────────────────
7
20
  ITEM_BASE = [
8
21
  *Control::BASE,
9
22
  "flex items-center gap-(--sp-space-2) w-full",
@@ -28,21 +41,10 @@ module StimulusPlumbers
28
41
  text-(length:--sp-text-xs) text-(--sp-color-muted-fg)
29
42
  ].freeze
30
43
 
31
- SECTION_TITLE_BASE = %w[
32
- block px-(--sp-space-2) pb-(--sp-space-1)
33
- text-(length:--sp-text-xs) font-semibold uppercase tracking-wider
34
- text-(--sp-color-muted-fg)
35
- ].freeze
36
-
37
- SECTION_DESCRIPTION_BASE = %w[
38
- block px-(--sp-space-2) pb-(--sp-space-1)
39
- text-(length:--sp-text-xs) text-(--sp-color-muted-fg)
40
- ].freeze
41
-
42
44
  private
43
45
 
44
46
  def list_classes
45
- { classes: klasses("py-(--sp-space-1) divide-y divide-(--sp-color-border)") }
47
+ { classes: klasses("py-(--sp-space-1)") }
46
48
  end
47
49
 
48
50
  def list_section_classes
@@ -62,7 +64,7 @@ module StimulusPlumbers
62
64
  end
63
65
 
64
66
  def list_item_icon_classes
65
- { classes: klasses("size-(--sp-control-size)", "stroke-current") }
67
+ { classes: klasses("size-(--sp-icon-size-sm)", "stroke-current") }
66
68
  end
67
69
 
68
70
  def list_item_content_classes
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Themes
5
+ module Tailwind
6
+ module Timeline
7
+ module Group
8
+ WRAPPER = %w[space-y-4].freeze
9
+ SECTION = %w[
10
+ p-4
11
+ bg-(--sp-color-bg-muted)
12
+ border border-(--sp-color-border)
13
+ rounded-lg
14
+ ].freeze
15
+ DATE = %w[text-base font-semibold text-(--sp-color-fg)].freeze
16
+ LIST = %w[mt-3 divide-y divide-(--sp-color-border)].freeze
17
+
18
+ private
19
+
20
+ def timeline_group_classes = { classes: klasses(WRAPPER) }
21
+ def timeline_group_section_classes = { classes: klasses(SECTION) }
22
+ def timeline_group_section_date_classes = { classes: klasses(DATE) }
23
+ def timeline_group_section_list_classes = { classes: klasses(LIST) }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "timeline/group"
4
+
5
+ module StimulusPlumbers
6
+ module Themes
7
+ module Tailwind
8
+ module Timeline
9
+ TRACK_VERTICAL = %w[flex flex-col gap-y-10].freeze
10
+ TRACK_HORIZONTAL = %w[flex].freeze
11
+
12
+ TRACK_LINE_VERTICAL = %w[absolute inset-y-0 start-3 w-px bg-(--sp-color-border)].freeze
13
+
14
+ ITEM_VERTICAL = %w[flex gap-x-4 items-start].freeze
15
+ ITEM_HORIZONTAL = %w[relative flex-1].freeze
16
+
17
+ ITEM_INDICATOR_DOT = %w[
18
+ mt-1 flex shrink-0 size-6 items-center justify-center rounded-full z-10
19
+ bg-(--sp-color-bg) ring-4 ring-(--sp-color-bg)
20
+ ].freeze
21
+ ITEM_INDICATOR_DOT_MARK = %w[size-3 rounded-full bg-(--sp-color-indicator)].freeze
22
+
23
+ ITEM_INDICATOR_ICON = %w[
24
+ mt-1 flex shrink-0 size-6 items-center justify-center rounded-full z-10
25
+ bg-(--sp-color-primary) text-(--sp-color-primary-fg)
26
+ ring-4 ring-(--sp-color-bg)
27
+ ].freeze
28
+
29
+ ITEM_INDICATOR_ICON_SLOT = %w[size-(--sp-icon-size-sm) stroke-current].freeze
30
+
31
+ ITEM_INDICATOR_DOT_HORIZONTAL = %w[
32
+ z-10 flex shrink-0 size-6 items-center justify-center rounded-full
33
+ bg-(--sp-color-bg) ring-4 ring-(--sp-color-bg)
34
+ ].freeze
35
+
36
+ ITEM_INDICATOR_ICON_HORIZONTAL = %w[
37
+ z-10 flex shrink-0 size-6 items-center justify-center rounded-full
38
+ bg-(--sp-color-primary) text-(--sp-color-primary-fg)
39
+ ring-4 ring-(--sp-color-bg)
40
+ ].freeze
41
+
42
+ ITEM_CONNECTOR_HORIZONTAL = %w[w-full h-px bg-(--sp-color-border) last:hidden].freeze
43
+ ITEM_CONTENT_HORIZONTAL = %w[mt-3 pe-2].freeze
44
+
45
+ ITEM_TIME = %w[mb-1 block text-sm leading-none text-(--sp-color-muted-fg)].freeze
46
+ ITEM_TIME_BADGE = %w[
47
+ mb-1 inline-block
48
+ bg-(--sp-color-bg-muted) border border-(--sp-color-border)
49
+ text-(--sp-color-fg) text-xs font-medium px-1.5 py-0.5 rounded
50
+ ].freeze
51
+ ITEM_TITLE = %w[mb-1 text-base font-semibold text-(--sp-color-fg)].freeze
52
+ ITEM_HEADING = %w[mb-1].freeze
53
+ ITEM_TRIGGER = %w[
54
+ w-full text-left text-base font-semibold
55
+ text-(--sp-color-fg)
56
+ hover:text-(--sp-color-primary)
57
+ focus-visible:outline-none focus-visible:ring-2
58
+ focus-visible:ring-(--sp-color-primary) focus-visible:rounded-sm
59
+ ].freeze
60
+ ITEM_DESCRIPTION = %w[text-sm text-(--sp-color-muted-fg)].freeze
61
+ ITEM_DETAIL = %w[mt-2 text-sm text-(--sp-color-muted-fg)].freeze
62
+ ITEM_ACTIONS = %w[mt-3 flex flex-wrap items-center gap-2].freeze
63
+
64
+ private
65
+
66
+ def timeline_classes(orientation: :vertical)
67
+ track = orientation.to_sym == :horizontal ? TRACK_HORIZONTAL : TRACK_VERTICAL
68
+ { classes: klasses(track) }
69
+ end
70
+
71
+ def timeline_item_classes(orientation: :vertical)
72
+ item = orientation.to_sym == :horizontal ? ITEM_HORIZONTAL : ITEM_VERTICAL
73
+ { classes: klasses(item) }
74
+ end
75
+
76
+ def timeline_item_indicator_classes(type: :dot, orientation: :vertical)
77
+ base = if orientation.to_sym == :horizontal
78
+ type.to_sym == :icon ? ITEM_INDICATOR_ICON_HORIZONTAL : ITEM_INDICATOR_DOT_HORIZONTAL
79
+ else
80
+ type.to_sym == :icon ? ITEM_INDICATOR_ICON : ITEM_INDICATOR_DOT
81
+ end
82
+ { classes: klasses(base) }
83
+ end
84
+
85
+ def timeline_item_time_classes(type: :default)
86
+ base = type.to_sym == :badge ? ITEM_TIME_BADGE : ITEM_TIME
87
+ { classes: klasses(base) }
88
+ end
89
+
90
+ def timeline_item_title_classes = { classes: klasses(ITEM_TITLE) }
91
+ def timeline_item_heading_classes = { classes: klasses(ITEM_HEADING) }
92
+ def timeline_item_trigger_classes = { classes: klasses(ITEM_TRIGGER) }
93
+ def timeline_item_description_classes = { classes: klasses(ITEM_DESCRIPTION) }
94
+ def timeline_item_detail_classes = { classes: klasses(ITEM_DETAIL) }
95
+ def timeline_item_actions_classes = { classes: klasses(ITEM_ACTIONS) }
96
+ def timeline_item_indicator_dot_classes = { classes: klasses(ITEM_INDICATOR_DOT_MARK) }
97
+ def timeline_item_connector_classes = { classes: klasses(ITEM_CONNECTOR_HORIZONTAL) }
98
+ def timeline_item_content_classes = { classes: klasses(ITEM_CONTENT_HORIZONTAL) }
99
+ def timeline_track_line_classes = { classes: klasses(TRACK_LINE_VERTICAL) }
100
+ def timeline_item_indicator_icon_slot_classes = { classes: klasses(ITEM_INDICATOR_ICON_SLOT) }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -14,6 +14,7 @@ require_relative "tailwind/form/input"
14
14
  require_relative "tailwind/icon"
15
15
  require_relative "tailwind/layout"
16
16
  require_relative "tailwind/link"
17
+ require_relative "tailwind/timeline"
17
18
 
18
19
  module StimulusPlumbers
19
20
  module Themes
@@ -31,6 +32,8 @@ module StimulusPlumbers
31
32
  include Tailwind::Icon
32
33
  include Tailwind::Layout
33
34
  include Tailwind::Link
35
+ include Tailwind::Timeline
36
+ include Tailwind::Timeline::Group
34
37
 
35
38
  private
36
39
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusPlumbersTailwind
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimulus_plumbers_tailwind
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Chang
@@ -698,6 +698,8 @@ files:
698
698
  - lib/stimulus_plumbers/themes/tailwind/layout.rb
699
699
  - lib/stimulus_plumbers/themes/tailwind/link.rb
700
700
  - lib/stimulus_plumbers/themes/tailwind/list.rb
701
+ - lib/stimulus_plumbers/themes/tailwind/timeline.rb
702
+ - lib/stimulus_plumbers/themes/tailwind/timeline/group.rb
701
703
  - lib/stimulus_plumbers/themes/tailwind_theme.rb
702
704
  - lib/stimulus_plumbers_tailwind.rb
703
705
  - lib/stimulus_plumbers_tailwind/engine.rb