tailmix 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +76 -145
- data/app/javascript/tailmix/runtime/action_dispatcher.js +132 -0
- data/app/javascript/tailmix/runtime/component.js +130 -0
- data/app/javascript/tailmix/runtime/index.js +130 -0
- data/app/javascript/tailmix/runtime/plugins.js +35 -0
- data/app/javascript/tailmix/runtime/updater.js +140 -0
- data/docs/01_getting_started.md +39 -0
- data/examples/_modal_component.arb +17 -25
- data/examples/button.rb +81 -0
- data/examples/helpers.rb +25 -0
- data/examples/modal_component.rb +32 -164
- data/lib/tailmix/definition/builders/action_builder.rb +39 -0
- data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
- data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
- data/lib/tailmix/definition/builders/component_builder.rb +81 -0
- data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
- data/lib/tailmix/definition/builders/element_builder.rb +83 -0
- data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
- data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
- data/lib/tailmix/definition/builders/state_builder.rb +21 -0
- data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
- data/lib/tailmix/definition/context_builder.rb +19 -13
- data/lib/tailmix/definition/merger.rb +2 -1
- data/lib/tailmix/definition/payload_proxy.rb +16 -0
- data/lib/tailmix/definition/result.rb +17 -18
- data/lib/tailmix/dev/docs.rb +32 -7
- data/lib/tailmix/dev/tools.rb +0 -5
- data/lib/tailmix/dsl.rb +3 -5
- data/lib/tailmix/engine.rb +11 -1
- data/lib/tailmix/html/attributes.rb +22 -12
- data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
- data/lib/tailmix/registry.rb +37 -0
- data/lib/tailmix/runtime/action_proxy.rb +29 -0
- data/lib/tailmix/runtime/attribute_builder.rb +102 -0
- data/lib/tailmix/runtime/attribute_cache.rb +23 -0
- data/lib/tailmix/runtime/context.rb +51 -47
- data/lib/tailmix/runtime/facade_builder.rb +8 -5
- data/lib/tailmix/runtime/state.rb +36 -0
- data/lib/tailmix/runtime/state_proxy.rb +34 -0
- data/lib/tailmix/runtime.rb +0 -1
- data/lib/tailmix/version.rb +1 -1
- data/lib/tailmix/view_helpers.rb +49 -0
- data/lib/tailmix.rb +4 -2
- metadata +34 -21
- data/app/javascript/tailmix/finder.js +0 -15
- data/app/javascript/tailmix/index.js +0 -7
- data/app/javascript/tailmix/mutator.js +0 -28
- data/app/javascript/tailmix/runner.js +0 -7
- data/app/javascript/tailmix/stimulus_adapter.js +0 -37
- data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
- data/lib/tailmix/definition/contexts/element_builder.rb +0 -41
- data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
- data/lib/tailmix/dev/stimulus_generator.rb +0 -124
- data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {Component} from './component';
|
|
2
|
+
import {PluginManager} from './plugins';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Tailmix global object that manages the lifecycle of components and plugins.
|
|
6
|
+
* It provides methods for starting the application, hydration of components,
|
|
7
|
+
*/
|
|
8
|
+
const Tailmix = {
|
|
9
|
+
_namedInstances: {},
|
|
10
|
+
_components: new Map(),
|
|
11
|
+
_definitions: {},
|
|
12
|
+
_pluginManager: new PluginManager(),
|
|
13
|
+
_observer: null,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Starts the Tailmix application by loading component definitions and initializing components.
|
|
17
|
+
*/
|
|
18
|
+
start() {
|
|
19
|
+
this.loadDefinitions();
|
|
20
|
+
this._namedInstances = {};
|
|
21
|
+
this._components.clear();
|
|
22
|
+
|
|
23
|
+
this.hydrate(document.body);
|
|
24
|
+
this.observeChanges();
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hydrate components within the specified root element.
|
|
29
|
+
* @param rootElement
|
|
30
|
+
*/
|
|
31
|
+
hydrate(rootElement) {
|
|
32
|
+
const componentElements = rootElement.querySelectorAll('[data-tailmix-component]');
|
|
33
|
+
componentElements.forEach(element => {
|
|
34
|
+
if (this._components.has(element)) return;
|
|
35
|
+
|
|
36
|
+
const componentName = element.dataset.tailmixComponent;
|
|
37
|
+
const definition = this._definitions[componentName];
|
|
38
|
+
if (!definition) { /* ... */
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const component = new Component(element, definition, this._pluginManager);
|
|
43
|
+
this._components.set(element, component);
|
|
44
|
+
|
|
45
|
+
const componentId = element.dataset.tailmixId;
|
|
46
|
+
if (componentId) {
|
|
47
|
+
this._namedInstances[componentId] = component;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// External trigger binding
|
|
52
|
+
const triggerElements = rootElement.querySelectorAll('[data-tailmix-trigger-for]');
|
|
53
|
+
triggerElements.forEach(element => {
|
|
54
|
+
const targetId = element.dataset.tailmixTriggerFor;
|
|
55
|
+
const targetComponent = this._namedInstances[targetId];
|
|
56
|
+
if (targetComponent) {
|
|
57
|
+
targetComponent.dispatcher.bindAction(element);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Loads and parses the component definitions from a script tag with the attribute `data-tailmix-definitions`.
|
|
64
|
+
* If a valid JSON content is found, it assigns it to the `definitions` variable.
|
|
65
|
+
* Logs an error message to the console in case of parsing failure.
|
|
66
|
+
*
|
|
67
|
+
* @return {void} Does not return a value.
|
|
68
|
+
*/
|
|
69
|
+
loadDefinitions() {
|
|
70
|
+
const definitionsTag = document.querySelector('script[data-tailmix-definitions]');
|
|
71
|
+
if (definitionsTag) {
|
|
72
|
+
try {
|
|
73
|
+
this._definitions = JSON.parse(definitionsTag.textContent);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error("Tailmix: Failed to parse component definitions.", e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieves a component associated with the nearest ancestor element
|
|
82
|
+
* that has the `data-tailmix-component` attribute.
|
|
83
|
+
*
|
|
84
|
+
* @param {Element} element - The DOM element from which the component search begins.
|
|
85
|
+
* @return {*} The component associated with the identified ancestor element, or `undefined` if no component is found.
|
|
86
|
+
*/
|
|
87
|
+
getComponent(element) {
|
|
88
|
+
const root = element.closest('[data-tailmix-component]');
|
|
89
|
+
return root ? this._components.get(root) : undefined;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
registerPlugin(plugin) {
|
|
93
|
+
this._pluginManager.register(plugin);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Observes changes in the DOM and rehydrates components when they are added or modified.
|
|
98
|
+
*/
|
|
99
|
+
observeChanges() {
|
|
100
|
+
if (this._observer) this._observer.disconnect();
|
|
101
|
+
|
|
102
|
+
this._observer = new MutationObserver(mutations => {
|
|
103
|
+
for (const mutation of mutations) {
|
|
104
|
+
if (mutation.type === 'childList') {
|
|
105
|
+
mutation.addedNodes.forEach(node => {
|
|
106
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
107
|
+
// If the component itself was added
|
|
108
|
+
if (node.matches('[data-tailmix-component]')) {
|
|
109
|
+
this.hydrate(node);
|
|
110
|
+
}
|
|
111
|
+
// If a parent was added, which may contain components
|
|
112
|
+
this.hydrate(node);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this._observer.observe(document.body, {childList: true, subtree: true});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Initialize Tailmix on page load
|
|
124
|
+
document.addEventListener("turbo:load", () => {
|
|
125
|
+
console.log("Tailmix starting...");
|
|
126
|
+
Tailmix.start();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export default Tailmix;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class PluginManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.plugins = [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers a plugin with the plugin manager.
|
|
8
|
+
* @param plugin
|
|
9
|
+
*/
|
|
10
|
+
register(plugin) {
|
|
11
|
+
if (!plugin.name) {
|
|
12
|
+
console.error("Tailmix Plugin Error: a plugin must have a name.", plugin);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.plugins.push(plugin);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connects a component to all registered plugins.
|
|
20
|
+
* @param component
|
|
21
|
+
*/
|
|
22
|
+
connect(component) {
|
|
23
|
+
this.plugins.forEach(plugin => {
|
|
24
|
+
if (typeof plugin.connect === 'function') {
|
|
25
|
+
const pluginConfig = component.definition.plugins?.[plugin.name];
|
|
26
|
+
if (pluginConfig) {
|
|
27
|
+
plugin.connect(component.api, pluginConfig);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {PluginManager};
|
|
35
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export class Updater {
|
|
2
|
+
constructor(component) {
|
|
3
|
+
this.component = component;
|
|
4
|
+
this.definition = component.definition;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
run(newState, oldState) {
|
|
8
|
+
for (const elementName in this.definition.elements) {
|
|
9
|
+
const elementNode = this.component.elements[elementName];
|
|
10
|
+
const elementDef = this.definition.elements[elementName];
|
|
11
|
+
|
|
12
|
+
if (!elementNode || !elementDef) continue;
|
|
13
|
+
|
|
14
|
+
this.updateElement(elementNode, elementDef, newState, oldState);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
updateElement(elementNode, elementDef, newState, oldState) {
|
|
19
|
+
this.updateClasses(elementNode, elementDef, newState);
|
|
20
|
+
this.updateAttributes(elementNode, elementDef, newState);
|
|
21
|
+
this.updateContent(elementNode, elementDef, newState);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
updateClasses(elementNode, elementDef, newState) {
|
|
25
|
+
const targetClasses = this.calculateTargetClasses(elementDef, newState);
|
|
26
|
+
const currentClasses = new Set(elementNode.classList);
|
|
27
|
+
|
|
28
|
+
// We compare current classes with target classes and apply the difference.
|
|
29
|
+
targetClasses.forEach(cls => {
|
|
30
|
+
if (!currentClasses.has(cls)) {
|
|
31
|
+
elementNode.classList.add(cls);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
currentClasses.forEach(cls => {
|
|
36
|
+
if (!targetClasses.has(cls)) {
|
|
37
|
+
// We avoid removing base classes that were originally in HTML.
|
|
38
|
+
// (This is a simple heuristic; it can be improved if base classes are in the definition)
|
|
39
|
+
const isBaseClass = !this.isVariantClass(elementDef, cls);
|
|
40
|
+
if (!targetClasses.has(cls) && !isBaseClass) {
|
|
41
|
+
elementNode.classList.remove(cls);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateAttributes(elementNode, elementDef, newState) {
|
|
48
|
+
if (elementDef.attribute_bindings) {
|
|
49
|
+
for (const attrName in elementDef.attribute_bindings) {
|
|
50
|
+
if (["text", "html"].includes(attrName)) continue;
|
|
51
|
+
|
|
52
|
+
const stateKey = elementDef.attribute_bindings[attrName];
|
|
53
|
+
const newValue = newState[stateKey];
|
|
54
|
+
|
|
55
|
+
// We update the attribute only if it has changed.
|
|
56
|
+
if (elementNode.getAttribute(attrName) !== newValue) {
|
|
57
|
+
if (newValue === null || newValue === undefined) {
|
|
58
|
+
elementNode.removeAttribute(attrName);
|
|
59
|
+
} else {
|
|
60
|
+
elementNode.setAttribute(attrName, newValue);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
updateContent(elementNode, elementDef, newState) {
|
|
68
|
+
const bindings = elementDef.attribute_bindings;
|
|
69
|
+
if (!bindings) return;
|
|
70
|
+
|
|
71
|
+
// Обработка `bind :text`
|
|
72
|
+
const textStateKey = bindings.text;
|
|
73
|
+
if (textStateKey !== undefined) {
|
|
74
|
+
const newText = newState[textStateKey] || '';
|
|
75
|
+
if (elementNode.textContent !== newText) {
|
|
76
|
+
elementNode.textContent = newText;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Обработка `bind :html`
|
|
81
|
+
const htmlStateKey = bindings.html;
|
|
82
|
+
if (htmlStateKey !== undefined) {
|
|
83
|
+
const newHtml = newState[htmlStateKey] || '';
|
|
84
|
+
if (elementNode.innerHTML !== newHtml) {
|
|
85
|
+
elementNode.innerHTML = newHtml;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
calculateTargetClasses(elementDef, state) {
|
|
91
|
+
const classes = new Set();
|
|
92
|
+
|
|
93
|
+
// 1. We add base classes (if any are in the definition).
|
|
94
|
+
// (We skip this for now, as they are already in the HTML)
|
|
95
|
+
|
|
96
|
+
// 2. We apply classes from active variants (dimensions).
|
|
97
|
+
if (elementDef.dimensions) {
|
|
98
|
+
for (const dimName in elementDef.dimensions) {
|
|
99
|
+
const dimDef = elementDef.dimensions[dimName];
|
|
100
|
+
const stateValue = state[dimName] !== undefined ? state[dimName] : dimDef.default;
|
|
101
|
+
|
|
102
|
+
const variantDef = dimDef.variants?.[stateValue];
|
|
103
|
+
if (variantDef?.classes) {
|
|
104
|
+
variantDef.classes.forEach(cls => classes.add(cls));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. We apply classes from active compound variants.
|
|
110
|
+
if (elementDef.compound_variants) {
|
|
111
|
+
elementDef.compound_variants.forEach(cv => {
|
|
112
|
+
const conditions = cv.on;
|
|
113
|
+
const modifications = cv.modifications;
|
|
114
|
+
|
|
115
|
+
const isMatch = Object.entries(conditions).every(([key, value]) => {
|
|
116
|
+
return state[key] === value;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (isMatch && modifications.classes) {
|
|
120
|
+
modifications.classes.forEach(cls => classes.add(cls));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return classes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isVariantClass(elementDef, className) {
|
|
129
|
+
if (elementDef.dimensions) {
|
|
130
|
+
for (const dimName in elementDef.dimensions) {
|
|
131
|
+
const dim = elementDef.dimensions[dimName];
|
|
132
|
+
for (const variantName in dim.variants) {
|
|
133
|
+
if (dim.variants[variantName].classes?.includes(className)) return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ... a check can also be added for compound_variants
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Getting Started with Tailmix
|
|
2
|
+
|
|
3
|
+
Welcome to Tailmix! This guide will walk you through installing the gem, setting up your project, and creating your first component.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
Tailmix is built on the idea of **co-location**. Instead of defining component styles in separate CSS, SCSS, or CSS-in-JS files, you define them directly within the Ruby class that represents your component. This creates self-contained, highly reusable, and easily maintainable UI components.
|
|
8
|
+
|
|
9
|
+
The core of Tailmix is a powerful and expressive **DSL (Domain-Specific Language)** that allows you to declaratively define how a component should look based on its properties or "variants".
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Getting started with Tailmix involves two simple steps: adding the gem and installing the JavaScript bridge.
|
|
14
|
+
|
|
15
|
+
### 1. Add the Gem
|
|
16
|
+
|
|
17
|
+
Add `tailmix` to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle add tailmix
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Install JavaScript Assets
|
|
24
|
+
|
|
25
|
+
Run the installer to set up the necessary JavaScript files for the client-side bridge (used by actions).
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bin/rails g tailmix:install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This command will add tailmix to your importmap.rb and ensure its JavaScript is available in your application.
|
|
32
|
+
|
|
33
|
+
### Your First Component: A Badge
|
|
34
|
+
Let's create a simple BadgeComponent to see Tailmix in action.
|
|
35
|
+
|
|
36
|
+
#### 1. Define the Component Class
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
# Arbre view:
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
modal_component = ModalComponent.new(open: false, id: :user_profile_modal)
|
|
4
|
+
ui = modal_component.ui
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
ui = modal.ui
|
|
6
|
+
button "Open Modal Outer", tailmix_trigger_for(:user_profile_modal, :toggle_open)
|
|
7
7
|
|
|
8
|
-
div ui.
|
|
9
|
-
|
|
10
|
-
div ui.open do
|
|
11
|
-
"open"
|
|
12
|
-
end
|
|
8
|
+
div ui.container.component do
|
|
9
|
+
button "Open Modal", ui.open_button
|
|
13
10
|
|
|
14
|
-
div ui.
|
|
15
|
-
|
|
16
|
-
span "Close"
|
|
17
|
-
end
|
|
11
|
+
div ui.base do
|
|
12
|
+
div ui.overlay
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"Your payment has been successfully submitted. We’ve sent you an email with all of the details of your order."
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
div ui.footer do
|
|
28
|
-
button ui.confirm_button do
|
|
29
|
-
span "Confirm Purchase"
|
|
30
|
-
div ui.spinner do
|
|
31
|
-
span "Loading..."
|
|
14
|
+
div ui.panel do
|
|
15
|
+
div ui.title do
|
|
16
|
+
h3 "Modal Title"
|
|
17
|
+
button ui.close_button do
|
|
18
|
+
text_node "✖"
|
|
32
19
|
end
|
|
33
20
|
end
|
|
21
|
+
|
|
22
|
+
div ui.body do
|
|
23
|
+
para "This is the main content of the modal. It's powered by the new Tailmix Runtime!"
|
|
24
|
+
end
|
|
34
25
|
end
|
|
35
26
|
end
|
|
36
27
|
end
|
|
28
|
+
|
data/examples/button.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/tailmix"
|
|
4
|
+
require_relative "helpers"
|
|
5
|
+
|
|
6
|
+
class Button
|
|
7
|
+
include Tailmix
|
|
8
|
+
|
|
9
|
+
tailmix do
|
|
10
|
+
element :button do
|
|
11
|
+
dimension :intent, default: :primary do
|
|
12
|
+
variant :primary, "bg-blue-500"
|
|
13
|
+
variant :danger, "bg-red-500"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
dimension :size, default: :medium do
|
|
17
|
+
variant :medium, "p-4"
|
|
18
|
+
variant :small, "p-2"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
compound_variant on: { intent: :danger, size: :small } do
|
|
22
|
+
classes "font-bold"
|
|
23
|
+
data special: "true"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :ui
|
|
29
|
+
def initialize(intent: :primary, size: :medium)
|
|
30
|
+
@ui = tailmix(intent: intent, size: size)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
puts "-" * 100
|
|
36
|
+
puts Button.dev.docs
|
|
37
|
+
|
|
38
|
+
# == Tailmix Docs for Button ==
|
|
39
|
+
# Signature: `initialize(intent: :primary, size: :medium)`
|
|
40
|
+
#
|
|
41
|
+
# Dimensions:
|
|
42
|
+
# - intent (default: :primary)
|
|
43
|
+
# - :primary:
|
|
44
|
+
# - classes : "bg-blue-500"
|
|
45
|
+
# - :danger:
|
|
46
|
+
# - classes : "bg-red-500"
|
|
47
|
+
# - size (default: :medium)
|
|
48
|
+
# - :medium:
|
|
49
|
+
# - classes : "p-4"
|
|
50
|
+
# - :small:
|
|
51
|
+
# - classes : "p-2"
|
|
52
|
+
#
|
|
53
|
+
# Compound Variants:
|
|
54
|
+
# - on element `:button`:
|
|
55
|
+
# - on: { intent: :danger, size: :small }
|
|
56
|
+
# - classes : "font-bold"
|
|
57
|
+
# - data: {:special=>"true"}
|
|
58
|
+
#
|
|
59
|
+
# No actions defined.
|
|
60
|
+
#
|
|
61
|
+
# button
|
|
62
|
+
# classes :-> bg-red-500 p-4
|
|
63
|
+
# data :-> {}
|
|
64
|
+
# aria :-> {}
|
|
65
|
+
# tailmix :-> {"data-tailmix-button"=>"intent:danger,size:medium"}
|
|
66
|
+
|
|
67
|
+
not_compound_component = Button.new(intent: :danger, size: :medium)
|
|
68
|
+
print_component_ui(not_compound_component)
|
|
69
|
+
# button
|
|
70
|
+
# classes :-> bg-red-500 p-4
|
|
71
|
+
# data :-> {}
|
|
72
|
+
# aria :-> {}
|
|
73
|
+
# tailmix :-> {"data-tailmix-button"=>"intent:danger,size:medium"}
|
|
74
|
+
|
|
75
|
+
compound_component = Button.new(intent: :danger, size: :small)
|
|
76
|
+
print_component_ui(compound_component)
|
|
77
|
+
# button
|
|
78
|
+
# classes :-> bg-red-500 p-2 font-bold
|
|
79
|
+
# data :-> {"data-special"=>"true"}
|
|
80
|
+
# aria :-> {}
|
|
81
|
+
# tailmix :-> {"data-tailmix-button"=>"intent:danger,size:small"}
|
data/examples/helpers.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def stringify_keys(obj)
|
|
4
|
+
case obj
|
|
5
|
+
when Hash
|
|
6
|
+
obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
|
|
7
|
+
when Array
|
|
8
|
+
obj.map { |v| stringify_keys(v) }
|
|
9
|
+
else
|
|
10
|
+
obj
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def print_component_ui(component_instance)
|
|
15
|
+
component_instance.class.dev.elements.each do |element_name|
|
|
16
|
+
element = component_instance.ui.send(element_name)
|
|
17
|
+
puts element_name
|
|
18
|
+
element.each_attribute do |attribute|
|
|
19
|
+
attribute.each do |key, value|
|
|
20
|
+
puts " #{key} :-> #{value}"
|
|
21
|
+
end
|
|
22
|
+
puts ""
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|