tailmix 0.4.5 → 0.4.7

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.
@@ -0,0 +1,266 @@
1
+
2
+ # DSL Reference
3
+
4
+ The Tailmix DSL is designed to be declarative, expressive, and intuitive. All definitions are placed within a `tailmix do ... end` block inside your component class.
5
+
6
+ ## `element`
7
+
8
+ The `element` method is the top-level keyword used to define a logical part or section of your component.
9
+
10
+ ### Syntax
11
+
12
+ ```ruby
13
+ element :name, "optional base classes" do
14
+ # ... further definitions for this element
15
+ end
16
+ ```
17
+
18
+ * **`:name` (Symbol)**: A unique name for the element, like `:wrapper`, `:title`, or `:close_button`. This name is used to access the element's attributes from the `ui` object (e.g., `ui.wrapper`).
19
+ * **`"base classes"` (String, optional)**: A space-separated string of CSS classes that will always be applied to this element, regardless of any variants.
20
+ * **`do ... end` block**: This is where you define the element's variants using `dimension`.
21
+
22
+ ### Example
23
+
24
+ ```ruby
25
+ tailmix do
26
+ # An element with no base classes
27
+ element :wrapper do
28
+ # ...
29
+ end
30
+
31
+ # An element with base classes
32
+ element :title, "text-lg font-bold text-gray-900"
33
+ end
34
+ ```
35
+
36
+ -----
37
+
38
+ ## `dimension`
39
+
40
+ A `dimension` defines an axis of variation for an element. It represents a property of the component that can change and affect its styling, such as `size`, `color`, or `state`. It must be defined inside an `element` block.
41
+
42
+ ### Syntax
43
+
44
+ ```ruby
45
+ dimension :name, default: :value do
46
+ # ... variant definitions
47
+ end
48
+ ```
49
+
50
+ * **`:name` (Symbol)**: The name of the dimension, like `:size` or `:intent`.
51
+ * **`default: :value` (optional)**: The default value for this dimension if none is provided when the component is initialized.
52
+
53
+ ### Example
54
+
55
+ ```ruby
56
+ element :button, "rounded-lg" do
57
+ dimension :size, default: :md do
58
+ # ... variants for :sm, :md, :lg
59
+ end
60
+
61
+ dimension :intent, default: :primary do
62
+ # ... variants for :primary, :secondary, :danger
63
+ end
64
+ end
65
+ ```
66
+
67
+ -----
68
+
69
+ ## `variant`
70
+
71
+ The `variant` method defines the specific styles that should be applied when a `dimension` has a certain value. It must be defined inside a `dimension` block.
72
+
73
+ ### Syntax
74
+
75
+ There are two forms for defining a variant:
76
+
77
+ **1. Simple (inline classes)**
78
+
79
+ ```ruby
80
+ variant :name, "classes to apply"
81
+ ```
82
+
83
+ **2. Advanced (with a block)**
84
+
85
+ ```ruby
86
+ variant :name, "optional base classes for this variant" do
87
+ classes "...", group: :optional_label
88
+ data key: "value"
89
+ aria key: "value"
90
+ end
91
+ ```
92
+
93
+ * **`:name` (Symbol|Boolean|String)**: The value of the dimension this variant corresponds to (e.g., `:md`, `true`).
94
+ * **`"classes"` (String)**: A space-separated string of CSS classes.
95
+ * **`do ... end` block**: For more complex definitions:
96
+ * **`classes`**: Can be called multiple times to logically group classes. The optional `group:` option is for documentation purposes.
97
+ * **`data`**: A hash to define `data-*` attributes.
98
+ * **`aria`**: A hash to define `aria-*` attributes for accessibility.
99
+
100
+ ### Example
101
+
102
+ ```ruby
103
+ dimension :size, default: :md do
104
+ # Simple variant
105
+ variant :sm, "px-2 py-1 text-sm"
106
+
107
+ # Advanced variant with a block
108
+ variant :md, "px-4 py-2" do
109
+ classes "text-base font-semibold", group: :typography
110
+ data size: "medium"
111
+ aria pressed: "false"
112
+ end
113
+ end
114
+ ```
115
+
116
+ -----
117
+
118
+ ## `compound_variant`
119
+
120
+ A `compound_variant` allows you to define styles that apply only when a specific **combination** of dimensions is active. This is crucial for handling interdependencies in a design system. It must be defined inside an `element` block.
121
+
122
+ ### Syntax
123
+
124
+ ```ruby
125
+ compound_variant on: { dimension1: :value, dimension2: :value } do
126
+ # ... modifications (classes, data, aria)
127
+ end
128
+ ```
129
+
130
+ * **`on: { ... }` (Hash)**: A hash specifying the exact conditions under which this rule should be applied.
131
+ * **`do ... end` block**: The modifications to apply. It uses the same `classes`, `data`, and `aria` methods as the `variant` block.
132
+
133
+ ### Example
134
+
135
+ This example makes an "outline" button's text and border color match its intent.
136
+
137
+ ```ruby
138
+ element :button do
139
+ dimension :intent, default: :primary do
140
+ variant :primary, "bg-blue-500 text-white border-blue-500"
141
+ variant :danger, "bg-red-500 text-white border-red-500"
142
+ end
143
+
144
+ dimension :look, default: :fill do
145
+ variant :outline, "bg-transparent"
146
+ end
147
+
148
+ # Apply these classes ONLY when look is :outline AND intent is :primary
149
+ compound_variant on: { look: :outline, intent: :primary } do
150
+ classes "text-blue-500"
151
+ end
152
+
153
+ # Apply these classes ONLY when look is :outline AND intent is :danger
154
+ compound_variant on: { look: :outline, intent: :danger } do
155
+ classes "text-red-500"
156
+ end
157
+ end
158
+ ```
159
+
160
+ -----
161
+
162
+ ## `action`
163
+
164
+ The `action` method defines a named set of imperative UI mutations that can be triggered at runtime. This is the core of the client-side bridge for optimistic updates with Hotwire/Turbo. It is defined at the top level of the `tailmix` block.
165
+
166
+ ### Syntax
167
+
168
+ ```ruby
169
+ action :name, method: :add | :remove | :toggle do
170
+ element :element_to_modify do
171
+ classes "..."
172
+ data key: "value"
173
+ end
174
+ end
175
+ ```
176
+
177
+ * **`:name` (Symbol)**: A unique name for the action, like `:show_spinner` or `:disable_form`.
178
+ * **`method:`**: The default operation for all modifications within the block (`:add`, `:remove`, or `:toggle`).
179
+ * **`element :name do ... end`**: Specifies which element the following modifications apply to.
180
+
181
+ ### Example
182
+
183
+ ```ruby
184
+ action :show_pending_state, method: :add do
185
+ element :submit_button do
186
+ classes "opacity-50 cursor-not-allowed"
187
+ data pending: true
188
+ end
189
+
190
+ element :spinner do
191
+ # You can override the default method for a specific modification
192
+ classes "hidden", method: :remove
193
+ end
194
+ end
195
+ ```
196
+
197
+ -----
198
+
199
+ ## `stimulus`
200
+
201
+ The `stimulus` DSL provides helpers for declaratively adding Stimulus `data-*` attributes. It is available inside any `element` block. The DSL is chainable and context-aware.
202
+
203
+ ### Setting the Context
204
+
205
+ Most helpers require a controller context to be set first. You can do this in two ways:
206
+
207
+ * **`.controller("name")`**: This is the primary method. It both adds a `data-controller="name"` attribute and sets the context for subsequent chained calls.
208
+ * **`.context("name")`**: This method only sets the context for subsequent calls without adding a new `data-controller` attribute. This is useful when you want to add targets or actions to an element that is inside the scope of a controller defined on a parent element.
209
+
210
+ ### Helpers (Context-Aware)
211
+
212
+ These methods must be called after `.controller()` or `.context()`.
213
+
214
+ #### **`.target("name")`**
215
+
216
+ Adds a target for the current controller context.
217
+
218
+ * **Syntax**: `.target("myTarget")`
219
+ * **Result**: `data-[controller-name]-target="myTarget"`
220
+
221
+ #### **`.action(event, method)` or `.action("specifier")`**
222
+
223
+ Adds an action for the current controller context. It has multiple forms for flexibility.
224
+
225
+ * **Syntax 1 (Tuple)**: `.action(:click, :open)` -\> `data-action="click->[controller-name]#open"`
226
+ * **Syntax 2 (Hash)**: `.action(click: :open, mouseenter: :highlight)` -\> `data-action="click->[controller-name]#open mouseenter->[controller-name]#highlight"`
227
+ * **Syntax 3 (Raw String)**: `.action("click->other-controller#doSomething")`
228
+
229
+ #### **`.value(:name, ...)`**
230
+
231
+ Adds a value for the current controller context. It can be a literal value, or it can be resolved dynamically from a method or a proc.
232
+
233
+ * **Syntax**:
234
+ * `.value(:my_value, value: "some-string")`
235
+ * `.value(:user_id, method: :current_user_id)`
236
+ * `.value(:timestamp, call: -> { Time.now.to_i })`
237
+ * **Result**: `data-[controller-name]-my-value-value="..."`
238
+
239
+ #### **`.action_payload(:action, as: :value_name)`**
240
+
241
+ A powerful helper that serializes a Tailmix `action` definition into a Stimulus value, making it available on the client-side.
242
+
243
+ * **Syntax**: `.action_payload(:disable_form, as: :disable_data)`
244
+ * **Result**: `data-[controller-name]-disable-data-value="{...json...}"`
245
+
246
+ ### Example
247
+
248
+ ```ruby
249
+ element :confirm_button, "px-4 py-2" do
250
+ stimulus
251
+ .controller("modal") # -> data-controller="modal"
252
+ .action(:click, :open) # -> data-action="click->modal#open"
253
+ .target("panel") # -> data-modal-target="panel"
254
+ .value(:url, value: "/items/1") # -> data-modal-url-value="/items/1"
255
+ .action_payload(:toggle, as: :toggle_data) # -> data-modal-toggle-data-value="{...}"
256
+ end
257
+
258
+ element :overlay do
259
+ # Assume `modal` controller is on a parent element
260
+ stimulus
261
+ .context("modal")
262
+ .action(:click, :close) # -> data-action="click->modal#close"
263
+ end
264
+ ```
265
+
266
+ -----
@@ -0,0 +1,88 @@
1
+ # Advanced Usage
2
+
3
+ This guide covers more advanced features of Tailmix, including component inheritance and using the developer tools.
4
+
5
+ ## Component Inheritance
6
+
7
+ One of the most powerful features of Tailmix is the ability to inherit and extend component definitions. This allows you to create a base set of components for your design system and then specialize them for specific use cases.
8
+
9
+ When a component class inherits from another, their `tailmix` definitions are intelligently merged:
10
+
11
+ * **Base Classes**: Are combined, with duplicates removed.
12
+ * **Dimensions & Variants**: Are merged. If a child defines a variant with the same name as a parent (e.g., `:sm`), the child's definition will completely **override** the parent's for that specific variant. New variants are added.
13
+ * **Compound Variants**: Are combined. Both parent and child compound variants will be applied.
14
+ * **Actions**: Are merged. If a child defines an action with the same name as a parent, the child's definition **overrides** the parent's.
15
+
16
+ ### Example: BaseButton and DangerButton
17
+
18
+ Let's define a `BaseButtonComponent` and then a more specific `DangerButtonComponent` that inherits from it.
19
+
20
+ **1. The Base Component**
21
+
22
+ ```ruby
23
+ # app/components/base_button_component.rb
24
+ class BaseButtonComponent
25
+ include Tailmix
26
+ attr_reader :ui
27
+
28
+ def initialize(size: :md)
29
+ @ui = tailmix(size: size)
30
+ end
31
+
32
+ tailmix do
33
+ element :button, "font-semibold border rounded" do
34
+ dimension :size, default: :md do
35
+ variant :sm, "px-2.5 py-1.5 text-xs"
36
+ variant :md, "px-3 py-2 text-sm"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ ```
42
+
43
+ **2. The Inherited Component**
44
+
45
+ The DangerButtonComponent inherits from BaseButtonComponent and only specifies what's different.
46
+
47
+ ```ruby
48
+ # app/components/danger_button_component.rb
49
+ class DangerButtonComponent < BaseButtonComponent
50
+ tailmix do
51
+ # This adds to the parent's base classes
52
+ element :button, "bg-red-500 text-white border-transparent hover:bg-red-600" do
53
+ # This adds a new variant to the :size dimension
54
+ dimension :size do
55
+ variant :lg, "px-4 py-2 text-base"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ **Resulting Definition for `DangerButtonComponent`:**
63
+
64
+ The `:button` element will have the combined base classes: `"font-semibold border rounded bg-red-500 ..."`.
65
+
66
+ The `:size` dimension will now have three variants available: `:sm`, `:md` (from the parent), and `:lg` (from the child).
67
+
68
+ ### Developer Tools
69
+
70
+ Tailmix includes a set of developer tools available via the `.dev` class method on your component.
71
+
72
+ `.dev.docs`
73
+
74
+ Prints a comprehensive summary of the component's definition, including its signature, dimensions, variants, compound variants, and actions. This is invaluable for understanding a component's API at a glance.
75
+
76
+ ```ruby
77
+ puts DangerButtonComponent.dev.docs
78
+ ```
79
+
80
+ `.dev.stimulus.scaffold`
81
+
82
+
83
+ Analyzes the component's `stimulus` definitions and generates a boilerplate Stimulus controller in your console, complete with all targets, values, and action methods.
84
+
85
+ ```ruby
86
+ puts MyModalComponent.dev.stimulus.scaffold
87
+ ```
88
+
@@ -0,0 +1,119 @@
1
+ # The Client-Side Bridge
2
+
3
+ Tailmix is primarily a server-side tool, but it includes a powerful "bridge" to your client-side JavaScript, enabling dynamic and optimistic UI updates. This bridge operates on several levels.
4
+
5
+ ## Level 1: The State Bridge
6
+
7
+ Every element defined in Tailmix automatically receives a `data-tailmix-*` attribute.
8
+
9
+ ```html
10
+ <button data-tailmix-button="size:md,intent:primary">
11
+ Click Me
12
+ </button>
13
+ ```
14
+
15
+ This attribute serves two purposes:
16
+
17
+ 1. A Stable Selector: You can reliably select this element in your JavaScript with `document.querySelector('[data-tailmix-button]')`. This is used internally by Tailmix actions.
18
+
19
+ 2. A State Indicator: The value of the attribute reflects the currently applied variants. Your JavaScript can read this to understand the element's state without needing extra data attributes.
20
+
21
+ **Example: Reading State in Stimulus**
22
+
23
+ ```js
24
+ // button_controller.js
25
+ import { Controller } from "@hotwired/stimulus"
26
+
27
+ export default class extends Controller {
28
+ connect() {
29
+ // e.g., "size:md,intent:primary"
30
+ const variants = this.element.dataset.tailmixButton;
31
+ console.log("Current button variants:", variants);
32
+
33
+ if (variants.includes("size:md")) {
34
+ // do something specific for medium buttons
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Level 2: The Action Bridge
41
+
42
+ This is the most powerful part of the bridge, allowing you to execute UI mutations defined in Ruby directly on the client.
43
+
44
+ 1. Define an `actio`n in Ruby
45
+
46
+ First, define an `action` in your component. This action describes a set of changes to apply to different elements.
47
+
48
+ ```ruby
49
+ # app/components/form_component.rb
50
+ class FormComponent
51
+ include Tailmix
52
+ tailmix do
53
+ element :submit_button
54
+ element :spinner, "hidden"
55
+
56
+ action :show_pending_state, method: :add do
57
+ element :submit_button do
58
+ classes "opacity-50 cursor-not-allowed"
59
+ end
60
+ element :spinner do
61
+ classes "hidden", method: :remove # override default method
62
+ end
63
+ end
64
+ end
65
+ end
66
+ ```
67
+
68
+ 2. Expose it with action_payload
69
+
70
+ Use the `action_payload` helper in your `stimulus` block to serialize the action's definition into a Stimulus value.
71
+
72
+ ```ruby
73
+ # ...inside the :submit_button element
74
+ stimulus
75
+ .controller("form")
76
+ .action(:click, :submit)
77
+ .action_payload(:show_pending_state, as: :pending_data)
78
+ ```
79
+
80
+ This will generate `data-form-pending-data-value="{...}"` containing the JSON definition of your `:show_pending_state` action.
81
+
82
+ 3. Run it from Stimulus
83
+
84
+ In your Stimulus controller, import `Tailmix` and call `Tailmix.run()` with the action's config.
85
+
86
+ ```js
87
+ // form_controller.js
88
+ import { Controller } from "@hotwired/stimulus"
89
+ import Tailmix from "tailmix"
90
+
91
+ export default class extends Controller {
92
+ static values = { pendingData: Object }
93
+
94
+ submit(event) {
95
+ event.preventDefault();
96
+
97
+ // Instantly apply the UI changes defined in Ruby
98
+ Tailmix.run({
99
+ config: this.pendingDataValue,
100
+ controller: this
101
+ });
102
+
103
+ // ... now submit the form via fetch/AJAX ...
104
+ }
105
+ }
106
+ ```
107
+
108
+ When `submit` is called, the button's classes will be changed and the spinner will be shown instantly, creating a fast, optimistic UI update.
109
+
110
+ ## Level 3: The Definition Bridge (Future Vision)
111
+
112
+ The ultimate goal is to allow the client to have access to the full `variant` and `compound_variant` definitions. This would enable the client to dynamically switch between any variant combination without a server roundtrip, providing a SPA-like experience.
113
+
114
+ This is a complex feature planned for a future release and will likely involve passing the definitions via an inline `<script type="application/json">` tag to keep the component's HTML clean.
115
+
116
+
117
+
118
+
119
+