rook-cli 1.3.2 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/package.json +3 -2
  2. package/rook-framework/PRD-INSTALL-COMMAND.md +379 -0
  3. package/rook-framework/PRD.md +1214 -0
  4. package/rook-framework/README.md +143 -0
  5. package/rook-framework/assets/rk-accordion.js +99 -0
  6. package/rook-framework/assets/rk-alert-dialog.js +132 -0
  7. package/rook-framework/assets/rk-bottom-app-bar.js +88 -0
  8. package/rook-framework/assets/rk-carousel.js +145 -0
  9. package/rook-framework/assets/rk-collapsible.js +151 -0
  10. package/rook-framework/assets/rk-dialog.js +161 -0
  11. package/rook-framework/assets/rk-drawer.js +214 -0
  12. package/rook-framework/assets/rk-framework-core.css +2554 -0
  13. package/rook-framework/assets/rk-framework-tokens.css +101 -0
  14. package/rook-framework/assets/rk-modal.js +91 -0
  15. package/rook-framework/assets/rk-popover.js +264 -0
  16. package/rook-framework/assets/rk-progress.js +81 -0
  17. package/rook-framework/assets/rk-quantity.js +91 -0
  18. package/rook-framework/assets/rk-scroll-area.js +286 -0
  19. package/rook-framework/assets/rk-sheet.js +157 -0
  20. package/rook-framework/assets/rk-tabs.js +179 -0
  21. package/rook-framework/assets/rk-toggle.js +153 -0
  22. package/rook-framework/blocks/rk-accordion.liquid +97 -0
  23. package/rook-framework/blocks/rk-badge.liquid +103 -0
  24. package/rook-framework/blocks/rk-button.liquid +166 -0
  25. package/rook-framework/blocks/rk-divider.liquid +100 -0
  26. package/rook-framework/blocks/rk-form-field.liquid +120 -0
  27. package/rook-framework/blocks/rk-icon.liquid +134 -0
  28. package/rook-framework/blocks/rk-image.liquid +198 -0
  29. package/rook-framework/blocks/rk-installments.liquid +99 -0
  30. package/rook-framework/blocks/rk-pix-discount.liquid +99 -0
  31. package/rook-framework/blocks/rk-price.liquid +128 -0
  32. package/rook-framework/blocks/rk-quantity.liquid +108 -0
  33. package/rook-framework/blocks/rk-quick-add.liquid +137 -0
  34. package/rook-framework/blocks/rk-skeleton.liquid +104 -0
  35. package/rook-framework/blocks/rk-typography.liquid +183 -0
  36. package/rook-framework/config/rk-color-scheme-group.json +138 -0
  37. package/rook-framework/config/rk-settings_schema.json +259 -0
  38. package/rook-framework/snippets/rk-accordion.liquid +31 -0
  39. package/rook-framework/snippets/rk-alert-dialog.liquid +83 -0
  40. package/rook-framework/snippets/rk-aspect-ratio.liquid +23 -0
  41. package/rook-framework/snippets/rk-badge.liquid +17 -0
  42. package/rook-framework/snippets/rk-bottom-app-bar.liquid +51 -0
  43. package/rook-framework/snippets/rk-button.liquid +49 -0
  44. package/rook-framework/snippets/rk-card.liquid +64 -0
  45. package/rook-framework/snippets/rk-carousel.liquid +74 -0
  46. package/rook-framework/snippets/rk-checkbox.liquid +34 -0
  47. package/rook-framework/snippets/rk-collapsible.liquid +52 -0
  48. package/rook-framework/snippets/rk-color-schemes-standalone.liquid +61 -0
  49. package/rook-framework/snippets/rk-color-schemes.liquid +43 -0
  50. package/rook-framework/snippets/rk-dialog.liquid +85 -0
  51. package/rook-framework/snippets/rk-divider.liquid +25 -0
  52. package/rook-framework/snippets/rk-drawer.liquid +81 -0
  53. package/rook-framework/snippets/rk-external-assets copy.liquid +33 -0
  54. package/rook-framework/snippets/rk-external-assets.liquid +68 -0
  55. package/rook-framework/snippets/rk-form-field.liquid +83 -0
  56. package/rook-framework/snippets/rk-gap-style.liquid +32 -0
  57. package/rook-framework/snippets/rk-icon.liquid +28 -0
  58. package/rook-framework/snippets/rk-image.liquid +60 -0
  59. package/rook-framework/snippets/rk-input.liquid +35 -0
  60. package/rook-framework/snippets/rk-installments.liquid +54 -0
  61. package/rook-framework/snippets/rk-item.liquid +69 -0
  62. package/rook-framework/snippets/rk-layout-style.liquid +37 -0
  63. package/rook-framework/snippets/rk-modal.liquid +31 -0
  64. package/rook-framework/snippets/rk-pix-discount.liquid +34 -0
  65. package/rook-framework/snippets/rk-popover.liquid +77 -0
  66. package/rook-framework/snippets/rk-price.liquid +48 -0
  67. package/rook-framework/snippets/rk-progress.liquid +38 -0
  68. package/rook-framework/snippets/rk-quantity.liquid +56 -0
  69. package/rook-framework/snippets/rk-quick-add.liquid +67 -0
  70. package/rook-framework/snippets/rk-scripts.liquid +17 -0
  71. package/rook-framework/snippets/rk-scroll-area.liquid +60 -0
  72. package/rook-framework/snippets/rk-sheet.liquid +86 -0
  73. package/rook-framework/snippets/rk-size-style.liquid +48 -0
  74. package/rook-framework/snippets/rk-skeleton.liquid +25 -0
  75. package/rook-framework/snippets/rk-spacing-padding.liquid +18 -0
  76. package/rook-framework/snippets/rk-spacing-style.liquid +54 -0
  77. package/rook-framework/snippets/rk-spinner.liquid +43 -0
  78. package/rook-framework/snippets/rk-swatch.liquid +33 -0
  79. package/rook-framework/snippets/rk-table.liquid +44 -0
  80. package/rook-framework/snippets/rk-tabs.liquid +52 -0
  81. package/rook-framework/snippets/rk-textarea.liquid +42 -0
  82. package/rook-framework/snippets/rk-toggle-group.liquid +27 -0
  83. package/rook-framework/snippets/rk-toggle.liquid +58 -0
  84. package/rook-framework/snippets/rk-typography.liquid +27 -0
  85. package/rook-framework/snippets/rk-variables.liquid +76 -0
  86. package/src/app.js +24 -0
  87. package/src/commands/InstallCommand.js +133 -0
  88. package/src/mcp/server.js +111 -1
  89. package/src/services/FrameworkInstaller.js +485 -0
  90. package/src/templates/block.liquid.txt +0 -15
  91. package/src/ui/PromptUI.js +15 -1
  92. package/src/utils/logger.js +1 -1
@@ -0,0 +1,101 @@
1
+ /* ==========================================================================
2
+ Rook UI Core Framework — Design Tokens (Estáticos)
3
+ ====================================================================
4
+ Este arquivo contém APENAS tokens estáticos que NÃO dependem de
5
+ settings do tema Shopify. Os valores dinâmicos (cores, border-radius,
6
+ transition-speed) são gerados pelo snippet rk-variables.liquid via
7
+ a tag {% style %}, garantindo compatibilidade total com a Shopify.
8
+
9
+ Ordem de carregamento no layout/theme.liquid:
10
+ 1. {% render 'rk-variables' %} ← CSS vars dinâmicas (Liquid)
11
+ 2. {{ 'rk-framework-tokens.css' | ... }} ← Este arquivo (estáticos)
12
+ 3. {{ 'rk-framework-core.css' | ... }} ← Estilos BEM dos componentes
13
+ ========================================================================== */
14
+
15
+ :root {
16
+
17
+ /* ================================================================
18
+ SPACING SCALE (fixo, não vem do schema)
19
+ ================================================================ */
20
+ --rk-space-2xs: 4px;
21
+ --rk-space-xs: 8px;
22
+ --rk-space-sm: 12px;
23
+ --rk-space-md: 16px;
24
+ --rk-space-lg: 24px;
25
+ --rk-space-xl: 32px;
26
+ --rk-space-2xl: 48px;
27
+
28
+ /* ================================================================
29
+ RESPONSIVE SCALING FACTORS
30
+ Usados por rk-spacing-style e rk-gap-style para reduzir
31
+ espaçamentos automaticamente em viewports menores.
32
+ ================================================================ */
33
+ --rk-spacing-scale: 1;
34
+ --rk-gap-scale: 1;
35
+
36
+ /* ================================================================
37
+ TYPOGRAPHY SCALE
38
+ ================================================================ */
39
+ --rk-font-size-xs: 0.75rem; /* 12px */
40
+ --rk-font-size-sm: 0.875rem; /* 14px */
41
+ --rk-font-size-md: 1rem; /* 16px */
42
+ --rk-font-size-lg: 1.125rem; /* 18px */
43
+ --rk-font-size-xl: 1.25rem; /* 20px */
44
+ --rk-font-size-2xl: 1.5rem; /* 24px */
45
+ --rk-font-size-3xl: 1.875rem; /* 30px */
46
+ --rk-font-size-4xl: 2.25rem; /* 36px */
47
+
48
+ /* ================================================================
49
+ LINE HEIGHT
50
+ ================================================================ */
51
+ --rk-line-height-none: 1;
52
+ --rk-line-height-tight: 1.25;
53
+ --rk-line-height-normal: 1.5;
54
+ --rk-line-height-relaxed: 1.625;
55
+ --rk-line-height-loose: 2;
56
+
57
+ /* ================================================================
58
+ BUTTON SIZING
59
+ ================================================================ */
60
+ --rk-btn-padding-sm: 8px 16px;
61
+ --rk-btn-padding-md: 12px 24px;
62
+ --rk-btn-padding-lg: 16px 32px;
63
+ --rk-btn-font-sm: var(--rk-font-size-sm);
64
+ --rk-btn-font-md: var(--rk-font-size-md);
65
+ --rk-btn-font-lg: var(--rk-font-size-lg);
66
+
67
+ /* ================================================================
68
+ Z-INDEX LAYERS
69
+ ================================================================ */
70
+ --rk-layer-base: 1;
71
+ --rk-layer-raised: 10;
72
+ --rk-layer-overlay: 100;
73
+ --rk-layer-popover: 500;
74
+ --rk-layer-drawer: 900;
75
+ --rk-layer-modal: 1000;
76
+ --rk-layer-alert: 1100;
77
+
78
+ /* ================================================================
79
+ OPACITY
80
+ ================================================================ */
81
+ --rk-opacity-disabled: 0.5;
82
+ --rk-opacity-muted: 0.7;
83
+ }
84
+
85
+ /* ==========================================================================
86
+ RESPONSIVE SCALING
87
+ Reduz espaçamentos e gaps automaticamente em viewports menores.
88
+ ========================================================================== */
89
+ @media (max-width: 989px) {
90
+ :root {
91
+ --rk-spacing-scale: 0.85;
92
+ --rk-gap-scale: 0.85;
93
+ }
94
+ }
95
+
96
+ @media (max-width: 749px) {
97
+ :root {
98
+ --rk-spacing-scale: 0.7;
99
+ --rk-gap-scale: 0.7;
100
+ }
101
+ }
@@ -0,0 +1,91 @@
1
+ /* ==========================================================================
2
+ Rook UI Core — Modal Controller
3
+ Web Component: <rk-modal-element>
4
+ ========================================================================== */
5
+
6
+ if (!customElements.get('rk-modal-element')) {
7
+ class RkModalElement extends HTMLElement {
8
+ constructor() {
9
+ super();
10
+ this.modal = null;
11
+ this.focusableElements = [];
12
+ this.previousFocus = null;
13
+ }
14
+
15
+ connectedCallback() {
16
+ this.modal = this.querySelector('.rk-modal');
17
+ if (!this.modal) return;
18
+
19
+ this.bindEvents();
20
+ this.setupOpenTriggers();
21
+ }
22
+
23
+ bindEvents() {
24
+ // Close buttons (overlay & X)
25
+ this.querySelectorAll('[data-action="close"]').forEach((el) => {
26
+ el.addEventListener('click', () => this.close());
27
+ });
28
+
29
+ // ESC key
30
+ document.addEventListener('keydown', (e) => {
31
+ if (e.key === 'Escape' && this.isOpen()) {
32
+ this.close();
33
+ }
34
+ });
35
+ }
36
+
37
+ setupOpenTriggers() {
38
+ const modalId = this.dataset.modalId || this.modal?.id;
39
+ if (!modalId) return;
40
+
41
+ document.querySelectorAll(`[data-modal-open="${modalId}"]`).forEach((trigger) => {
42
+ trigger.addEventListener('click', (e) => {
43
+ e.preventDefault();
44
+ this.open();
45
+ });
46
+ });
47
+ }
48
+
49
+ isOpen() {
50
+ return this.modal?.classList.contains('rk-modal--active');
51
+ }
52
+
53
+ open() {
54
+ if (!this.modal) return;
55
+
56
+ this.previousFocus = document.activeElement;
57
+ this.modal.classList.add('rk-modal--active');
58
+ document.body.style.overflow = 'hidden';
59
+
60
+ // Trap focus
61
+ this.focusableElements = this.modal.querySelectorAll(
62
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
63
+ );
64
+
65
+ if (this.focusableElements.length > 0) {
66
+ this.focusableElements[0].focus();
67
+ }
68
+
69
+ this.dispatchEvent(
70
+ new CustomEvent('rk:modal:open', { bubbles: true, detail: { id: this.modal.id } })
71
+ );
72
+ }
73
+
74
+ close() {
75
+ if (!this.modal) return;
76
+
77
+ this.modal.classList.remove('rk-modal--active');
78
+ document.body.style.overflow = '';
79
+
80
+ if (this.previousFocus) {
81
+ this.previousFocus.focus();
82
+ }
83
+
84
+ this.dispatchEvent(
85
+ new CustomEvent('rk:modal:close', { bubbles: true, detail: { id: this.modal.id } })
86
+ );
87
+ }
88
+ }
89
+
90
+ customElements.define('rk-modal-element', RkModalElement);
91
+ }
@@ -0,0 +1,264 @@
1
+ /* ==========================================================================
2
+ Rook UI Core — Popover Controller
3
+ Web Component: <rk-popover-element>
4
+ ========================================================================== */
5
+
6
+ if (!customElements.get('rk-popover-element')) {
7
+ class RkPopoverElement extends HTMLElement {
8
+ constructor() {
9
+ super();
10
+ this.trigger = null;
11
+ this.content = null;
12
+ this.previousFocus = null;
13
+ this._onClickOutside = this._onClickOutside.bind(this);
14
+ this._onKeyDown = this._onKeyDown.bind(this);
15
+ this._onResize = this._debounce(this._reposition.bind(this), 100);
16
+ }
17
+
18
+ connectedCallback() {
19
+ this.trigger = this.querySelector('[data-rk-popover-trigger]');
20
+ this.content = this.querySelector('.rk-popover__content');
21
+
22
+ if (!this.trigger || !this.content) return;
23
+
24
+ this.trigger.addEventListener('click', (e) => {
25
+ e.preventDefault();
26
+ e.stopPropagation();
27
+ this.toggle();
28
+ });
29
+
30
+ // Ensure trigger has a11y attributes
31
+ this.trigger.setAttribute('aria-haspopup', 'dialog');
32
+ this.trigger.setAttribute('aria-expanded', 'false');
33
+
34
+ // Link trigger to content
35
+ const contentId = this.content.id || `rk-popover-${this._uid()}`;
36
+ this.content.id = contentId;
37
+ this.trigger.setAttribute('aria-controls', contentId);
38
+ }
39
+
40
+ disconnectedCallback() {
41
+ this._removeGlobalListeners();
42
+ }
43
+
44
+ /* ------------------------------------------------------------------ */
45
+ /* Public API */
46
+ /* ------------------------------------------------------------------ */
47
+
48
+ toggle() {
49
+ this.isOpen() ? this.close() : this.open();
50
+ }
51
+
52
+ isOpen() {
53
+ return this.content?.classList.contains('rk-popover__content--active');
54
+ }
55
+
56
+ open() {
57
+ if (!this.content || this.isOpen()) return;
58
+
59
+ this.previousFocus = document.activeElement;
60
+ this.content.classList.add('rk-popover__content--active');
61
+ this.trigger?.setAttribute('aria-expanded', 'true');
62
+
63
+ this._reposition();
64
+ this._addGlobalListeners();
65
+
66
+ // Trap focus — focus first interactive element inside
67
+ requestAnimationFrame(() => {
68
+ const firstFocusable = this.content.querySelector(
69
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
70
+ );
71
+ if (firstFocusable) {
72
+ firstFocusable.focus();
73
+ }
74
+ });
75
+
76
+ this.dispatchEvent(
77
+ new CustomEvent('rk:popover:open', { bubbles: true, detail: { id: this.content.id } })
78
+ );
79
+ }
80
+
81
+ close() {
82
+ if (!this.content || !this.isOpen()) return;
83
+
84
+ this.content.classList.remove('rk-popover__content--active');
85
+ this.trigger?.setAttribute('aria-expanded', 'false');
86
+
87
+ this._removeGlobalListeners();
88
+
89
+ // Restore focus
90
+ if (this.previousFocus) {
91
+ this.previousFocus.focus();
92
+ this.previousFocus = null;
93
+ }
94
+
95
+ this.dispatchEvent(
96
+ new CustomEvent('rk:popover:close', { bubbles: true, detail: { id: this.content.id } })
97
+ );
98
+ }
99
+
100
+ /* ------------------------------------------------------------------ */
101
+ /* Positioning Engine */
102
+ /* ------------------------------------------------------------------ */
103
+
104
+ _reposition() {
105
+ if (!this.trigger || !this.content || !this.isOpen()) return;
106
+
107
+ const preferredSide = this.dataset.side || 'bottom';
108
+ const align = this.dataset.align || 'center';
109
+ const sideOffset = parseInt(this.dataset.sideOffset, 10) || 8;
110
+ const alignOffset = parseInt(this.dataset.alignOffset, 10) || 0;
111
+
112
+ const triggerRect = this.trigger.getBoundingClientRect();
113
+ const contentRect = this.content.getBoundingClientRect();
114
+ const viewportW = window.innerWidth;
115
+ const viewportH = window.innerHeight;
116
+
117
+ // Determine actual side (with collision detection / auto-flip)
118
+ const side = this._resolveSide(preferredSide, triggerRect, contentRect, viewportW, viewportH, sideOffset);
119
+
120
+ let top = 0;
121
+ let left = 0;
122
+
123
+ // 1. Calculate position on the main axis (side)
124
+ switch (side) {
125
+ case 'top':
126
+ top = -(contentRect.height + sideOffset);
127
+ break;
128
+ case 'bottom':
129
+ top = triggerRect.height + sideOffset;
130
+ break;
131
+ case 'left':
132
+ left = -(contentRect.width + sideOffset);
133
+ break;
134
+ case 'right':
135
+ left = triggerRect.width + sideOffset;
136
+ break;
137
+ }
138
+
139
+ // 2. Calculate position on the cross axis (align)
140
+ if (side === 'top' || side === 'bottom') {
141
+ switch (align) {
142
+ case 'start':
143
+ left = alignOffset;
144
+ break;
145
+ case 'center':
146
+ left = (triggerRect.width - contentRect.width) / 2 + alignOffset;
147
+ break;
148
+ case 'end':
149
+ left = triggerRect.width - contentRect.width + alignOffset;
150
+ break;
151
+ }
152
+ } else {
153
+ switch (align) {
154
+ case 'start':
155
+ top = alignOffset;
156
+ break;
157
+ case 'center':
158
+ top = (triggerRect.height - contentRect.height) / 2 + alignOffset;
159
+ break;
160
+ case 'end':
161
+ top = triggerRect.height - contentRect.height + alignOffset;
162
+ break;
163
+ }
164
+ }
165
+
166
+ // 3. Clamp to viewport edges (prevent overflow)
167
+ const absLeft = triggerRect.left + left;
168
+ const absTop = triggerRect.top + top;
169
+
170
+ if (absLeft < 8) {
171
+ left += (8 - absLeft);
172
+ } else if (absLeft + contentRect.width > viewportW - 8) {
173
+ left -= (absLeft + contentRect.width - viewportW + 8);
174
+ }
175
+
176
+ if (absTop < 8) {
177
+ top += (8 - absTop);
178
+ } else if (absTop + contentRect.height > viewportH - 8) {
179
+ top -= (absTop + contentRect.height - viewportH + 8);
180
+ }
181
+
182
+ this.content.style.top = `${top}px`;
183
+ this.content.style.left = `${left}px`;
184
+
185
+ // Set data-side for arrow / animation CSS
186
+ this.content.dataset.side = side;
187
+ }
188
+
189
+ _resolveSide(preferred, triggerRect, contentRect, vw, vh, offset) {
190
+ const space = {
191
+ top: triggerRect.top - offset,
192
+ bottom: vh - triggerRect.bottom - offset,
193
+ left: triggerRect.left - offset,
194
+ right: vw - triggerRect.right - offset,
195
+ };
196
+
197
+ const fits = {
198
+ top: space.top >= contentRect.height,
199
+ bottom: space.bottom >= contentRect.height,
200
+ left: space.left >= contentRect.width,
201
+ right: space.right >= contentRect.width,
202
+ };
203
+
204
+ if (fits[preferred]) return preferred;
205
+
206
+ // Flip to opposite side
207
+ const opposite = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
208
+ if (fits[opposite[preferred]]) return opposite[preferred];
209
+
210
+ // Fallback: pick side with most space
211
+ const sorted = Object.entries(space).sort((a, b) => b[1] - a[1]);
212
+ return sorted[0][0];
213
+ }
214
+
215
+ /* ------------------------------------------------------------------ */
216
+ /* Global Listeners */
217
+ /* ------------------------------------------------------------------ */
218
+
219
+ _addGlobalListeners() {
220
+ document.addEventListener('click', this._onClickOutside, true);
221
+ document.addEventListener('keydown', this._onKeyDown);
222
+ window.addEventListener('resize', this._onResize);
223
+ window.addEventListener('scroll', this._onResize, true);
224
+ }
225
+
226
+ _removeGlobalListeners() {
227
+ document.removeEventListener('click', this._onClickOutside, true);
228
+ document.removeEventListener('keydown', this._onKeyDown);
229
+ window.removeEventListener('resize', this._onResize);
230
+ window.removeEventListener('scroll', this._onResize, true);
231
+ }
232
+
233
+ _onClickOutside(e) {
234
+ if (!this.contains(e.target)) {
235
+ this.close();
236
+ }
237
+ }
238
+
239
+ _onKeyDown(e) {
240
+ if (e.key === 'Escape' && this.isOpen()) {
241
+ e.preventDefault();
242
+ this.close();
243
+ }
244
+ }
245
+
246
+ /* ------------------------------------------------------------------ */
247
+ /* Helpers */
248
+ /* ------------------------------------------------------------------ */
249
+
250
+ _uid() {
251
+ return Math.random().toString(36).substring(2, 9);
252
+ }
253
+
254
+ _debounce(fn, delay) {
255
+ let timer;
256
+ return (...args) => {
257
+ clearTimeout(timer);
258
+ timer = setTimeout(() => fn(...args), delay);
259
+ };
260
+ }
261
+ }
262
+
263
+ customElements.define('rk-popover-element', RkPopoverElement);
264
+ }
@@ -0,0 +1,81 @@
1
+ /* ==========================================================================
2
+ Rook UI Core — Progress Controller
3
+ Web Component: <rk-progress-element>
4
+ ========================================================================== */
5
+
6
+ if (!customElements.get('rk-progress-element')) {
7
+ class RkProgressElement extends HTMLElement {
8
+ constructor() {
9
+ super();
10
+ this.indicator = null;
11
+ }
12
+
13
+ connectedCallback() {
14
+ this.indicator = this.querySelector('.rk-progress__indicator');
15
+ if (!this.indicator) return;
16
+
17
+ // Initialize position
18
+ this._updateIndicator();
19
+
20
+ // Observe data attribute changes for reactive updates
21
+ this.observer = new MutationObserver((mutations) => {
22
+ mutations.forEach((mutation) => {
23
+ if (mutation.type === 'attributes' &&
24
+ (mutation.attributeName === 'data-value' || mutation.attributeName === 'data-max')) {
25
+ this._updateIndicator();
26
+ }
27
+ });
28
+ });
29
+
30
+ this.observer.observe(this, { attributes: true });
31
+ }
32
+
33
+ disconnectedCallback() {
34
+ if (this.observer) {
35
+ this.observer.disconnect();
36
+ }
37
+ }
38
+
39
+ /* ------------------------------------------------------------------ */
40
+ /* Public API */
41
+ /* ------------------------------------------------------------------ */
42
+
43
+ get value() {
44
+ const val = parseFloat(this.getAttribute('data-value'));
45
+ return isNaN(val) ? 0 : val;
46
+ }
47
+
48
+ set value(val) {
49
+ this.setAttribute('data-value', val);
50
+ }
51
+
52
+ get max() {
53
+ const m = parseFloat(this.getAttribute('data-max'));
54
+ return (isNaN(m) || m <= 0) ? 100 : m;
55
+ }
56
+
57
+ set max(m) {
58
+ this.setAttribute('data-max', m);
59
+ }
60
+
61
+ _updateIndicator() {
62
+ let percentage = (this.value / this.max) * 100;
63
+
64
+ // Clamp between 0 and 100
65
+ percentage = Math.max(0, Math.min(100, percentage));
66
+
67
+ // Update ARIA
68
+ const progressBar = this.querySelector('[role="progressbar"]');
69
+ if (progressBar) {
70
+ progressBar.setAttribute('aria-valuenow', this.value);
71
+ progressBar.setAttribute('aria-valuemax', this.max);
72
+ }
73
+
74
+ // Animate using transform (hardware accelerated)
75
+ // By default, it's translated to -100%. We calculate how much to translate back towards 0%.
76
+ this.indicator.style.transform = `translateX(-${100 - percentage}%)`;
77
+ }
78
+ }
79
+
80
+ customElements.define('rk-progress-element', RkProgressElement);
81
+ }
@@ -0,0 +1,91 @@
1
+ /* ==========================================================================
2
+ Rook UI Core — Quantity Selector Controller
3
+ Web Component: <rk-quantity-selector>
4
+ ========================================================================== */
5
+
6
+ if (!customElements.get('rk-quantity-selector')) {
7
+ class RkQuantitySelector extends HTMLElement {
8
+ constructor() {
9
+ super();
10
+ this.input = null;
11
+ this.btnMinus = null;
12
+ this.btnPlus = null;
13
+ }
14
+
15
+ connectedCallback() {
16
+ this.input = this.querySelector('.rk-quantity__input');
17
+ this.btnMinus = this.querySelector('[data-type="minus"]');
18
+ this.btnPlus = this.querySelector('[data-type="plus"]');
19
+
20
+ if (!this.input) return;
21
+
22
+ this.bindEvents();
23
+ this.updateButtons();
24
+ }
25
+
26
+ bindEvents() {
27
+ this.btnMinus?.addEventListener('click', () => this.step(-1));
28
+ this.btnPlus?.addEventListener('click', () => this.step(1));
29
+
30
+ this.input.addEventListener('change', () => {
31
+ this.clampValue();
32
+ this.updateButtons();
33
+ this.dispatchChange();
34
+ });
35
+
36
+ // Prevent non-numeric input
37
+ this.input.addEventListener('keydown', (e) => {
38
+ if (['e', 'E', '+', '-', '.', ','].includes(e.key)) {
39
+ e.preventDefault();
40
+ }
41
+ });
42
+ }
43
+
44
+ step(direction) {
45
+ const stepValue = parseInt(this.input.step) || 1;
46
+ const current = parseInt(this.input.value) || 1;
47
+ const newValue = current + (stepValue * direction);
48
+
49
+ this.input.value = newValue;
50
+ this.clampValue();
51
+ this.updateButtons();
52
+ this.dispatchChange();
53
+ }
54
+
55
+ clampValue() {
56
+ const min = parseInt(this.input.min) || 1;
57
+ const max = this.input.max ? parseInt(this.input.max) : Infinity;
58
+ let value = parseInt(this.input.value) || min;
59
+
60
+ value = Math.max(min, Math.min(max, value));
61
+ this.input.value = value;
62
+ }
63
+
64
+ updateButtons() {
65
+ const value = parseInt(this.input.value) || 1;
66
+ const min = parseInt(this.input.min) || 1;
67
+ const max = this.input.max ? parseInt(this.input.max) : Infinity;
68
+
69
+ if (this.btnMinus) {
70
+ this.btnMinus.disabled = value <= min;
71
+ }
72
+ if (this.btnPlus) {
73
+ this.btnPlus.disabled = value >= max;
74
+ }
75
+ }
76
+
77
+ dispatchChange() {
78
+ this.dispatchEvent(
79
+ new CustomEvent('rk:quantity:change', {
80
+ bubbles: true,
81
+ detail: {
82
+ value: parseInt(this.input.value),
83
+ name: this.input.name,
84
+ },
85
+ })
86
+ );
87
+ }
88
+ }
89
+
90
+ customElements.define('rk-quantity-selector', RkQuantitySelector);
91
+ }