mtrl 0.0.0 → 0.0.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.
@@ -1,9 +1,31 @@
1
1
  // src/styles/abstract/_mixins.scss
2
2
  @use 'sass:map';
3
3
  @use 'sass:list';
4
+ @use 'sass:math';
4
5
  @use 'variables' as v;
5
6
  @use 'functions' as f;
6
7
 
8
+ // Common icons map
9
+ $icons: (
10
+ 'chevron_right': 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="9 18 15 12 9 6"%3E%3C/polyline%3E%3C/svg%3E',
11
+ 'chevron_down': 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E',
12
+ 'check': 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="20 6 9 17 4 12"%3E%3C/polyline%3E%3C/svg%3E'
13
+ );
14
+
15
+ @mixin icon($name, $size: 24px) {
16
+ $icon-url: map.get($icons, $name);
17
+ @if $icon-url {
18
+ content: '';
19
+ display: inline-block;
20
+ width: $size;
21
+ height: $size;
22
+ background-image: url($icon-url);
23
+ background-size: contain;
24
+ background-position: center;
25
+ background-repeat: no-repeat;
26
+ }
27
+ }
28
+
7
29
  // Typography
8
30
  @mixin typography($scale) {
9
31
  $styles: f.get-typography($scale);
@@ -253,6 +275,59 @@
253
275
  }
254
276
  }
255
277
 
278
+
279
+ // Scrollbar mixin for consistent styling across components
280
+ @mixin scrollbar(
281
+ $width: 8px,
282
+ $track-color: rgba(0, 0, 0, 0.05),
283
+ $thumb-color: rgba(0, 0, 0, 0.2)
284
+ ) {
285
+ &::-webkit-scrollbar {
286
+ width: $width;
287
+ }
288
+
289
+ &::-webkit-scrollbar-track {
290
+ background: $track-color;
291
+ }
292
+
293
+ &::-webkit-scrollbar-thumb {
294
+ background: $thumb-color;
295
+ border-radius: math.div($width, 2);
296
+ }
297
+
298
+ // Firefox scrollbar (future compatibility)
299
+ scrollbar-width: thin;
300
+ scrollbar-color: $thumb-color $track-color;
301
+ }
302
+
303
+
304
+ // Flexbox layout mixins
305
+ @mixin flex-row($align: center, $justify: flex-start, $gap: 0) {
306
+ display: flex;
307
+ flex-direction: row;
308
+ align-items: $align;
309
+ justify-content: $justify;
310
+ @if $gap > 0 {
311
+ gap: $gap;
312
+ }
313
+ }
314
+
315
+ @mixin flex-column($align: flex-start, $justify: flex-start, $gap: 0) {
316
+ display: flex;
317
+ flex-direction: column;
318
+ align-items: $align;
319
+ justify-content: $justify;
320
+ @if $gap > 0 {
321
+ gap: $gap;
322
+ }
323
+ }
324
+
325
+ @mixin flex-center {
326
+ display: flex;
327
+ align-items: center;
328
+ justify-content: center;
329
+ }
330
+
256
331
  // Print
257
332
  @mixin print {
258
333
  @media print {
@@ -151,6 +151,7 @@ $z-index: (
151
151
  'dialog': 700,
152
152
  'dropdown': 600,
153
153
  'tooltip': 500,
154
+ 'menu': 700,
154
155
  'sticky': 100,
155
156
  'fixed': 50,
156
157
  'default': 1,
@@ -0,0 +1,158 @@
1
+ // test/components/button.test.js
2
+ import { describe, test, expect, mock } from 'bun:test'
3
+ import createButton from '../../src/components/button/button'
4
+
5
+ describe('Button Component', () => {
6
+ test('should create a button element', () => {
7
+ const button = createButton()
8
+ expect(button.element).toBeDefined()
9
+ expect(button.element.tagName).toBe('BUTTON')
10
+ expect(button.element.className).toContain('mtrl-button')
11
+ })
12
+
13
+ test('should add text content', () => {
14
+ const buttonText = 'Click Me'
15
+ const button = createButton({
16
+ text: buttonText
17
+ })
18
+
19
+ const textElement = button.element.querySelector('.mtrl-button-text')
20
+ expect(textElement).toBeDefined()
21
+ expect(textElement.textContent).toBe(buttonText)
22
+ })
23
+
24
+ test('should apply variant class', () => {
25
+ const variant = 'filled'
26
+ const button = createButton({
27
+ variant
28
+ })
29
+
30
+ expect(button.element.className).toContain(`mtrl-button--${variant}`)
31
+ })
32
+
33
+ test('should handle click events', () => {
34
+ const button = createButton()
35
+ const handleClick = mock(() => {})
36
+
37
+ button.on('click', handleClick)
38
+
39
+ // Simulate click event
40
+ const event = new Event('click')
41
+ button.element.dispatchEvent(event)
42
+
43
+ expect(handleClick).toHaveBeenCalled()
44
+ })
45
+
46
+ test('should support disabled state', () => {
47
+ const button = createButton()
48
+
49
+ // Initially not disabled
50
+ expect(button.element.hasAttribute('disabled')).toBe(false)
51
+
52
+ // Disable the button
53
+ button.disable()
54
+ expect(button.element.hasAttribute('disabled')).toBe(true)
55
+
56
+ // Check that the disabled property is also set
57
+ expect(button.element.disabled).toBe(true)
58
+
59
+ // Enable the button
60
+ button.enable()
61
+ expect(button.element.hasAttribute('disabled')).toBe(false)
62
+ expect(button.element.disabled).toBe(false)
63
+ })
64
+
65
+ test('should add icon content', () => {
66
+ const iconSvg = '<svg><path d="M10 10"></path></svg>'
67
+ const button = createButton({
68
+ icon: iconSvg
69
+ })
70
+
71
+ const iconElement = button.element.querySelector('.mtrl-button-icon')
72
+ expect(iconElement).toBeDefined()
73
+ expect(iconElement.innerHTML).toBe(iconSvg)
74
+ })
75
+
76
+ test('should position icon correctly', () => {
77
+ const iconSvg = '<svg><path d="M10 10"></path></svg>'
78
+
79
+ // Test end position
80
+ const endButton = createButton({
81
+ text: 'End Icon',
82
+ icon: iconSvg,
83
+ iconPosition: 'end'
84
+ })
85
+
86
+ const textElement = endButton.element.querySelector('.mtrl-button-text')
87
+ const iconElement = endButton.element.querySelector('.mtrl-button-icon')
88
+
89
+ // In the DOM, for end position, the text should come before the icon
90
+ const children = Array.from(endButton.element.childNodes)
91
+ const textIndex = children.indexOf(textElement)
92
+ const iconIndex = children.indexOf(iconElement)
93
+
94
+ expect(textIndex).toBeLessThan(iconIndex)
95
+
96
+ // Test start position
97
+ const startButton = createButton({
98
+ text: 'Start Icon',
99
+ icon: iconSvg,
100
+ iconPosition: 'start'
101
+ })
102
+
103
+ const startTextElement = startButton.element.querySelector('.mtrl-button-text')
104
+ const startIconElement = startButton.element.querySelector('.mtrl-button-icon')
105
+
106
+ const startChildren = Array.from(startButton.element.childNodes)
107
+ const startTextIndex = startChildren.indexOf(startTextElement)
108
+ const startIconIndex = startChildren.indexOf(startIconElement)
109
+
110
+ expect(startIconIndex).toBeLessThan(startTextIndex)
111
+ })
112
+
113
+ test('should support different sizes', () => {
114
+ const sizes = ['small', 'medium', 'large']
115
+
116
+ sizes.forEach(size => {
117
+ const button = createButton({
118
+ size
119
+ })
120
+
121
+ expect(button.element.className).toContain(`mtrl-button--${size}`)
122
+ })
123
+ })
124
+
125
+ test('should allow updating text', () => {
126
+ const button = createButton({
127
+ text: 'Initial'
128
+ })
129
+
130
+ const newText = 'Updated Text'
131
+ button.setText(newText)
132
+
133
+ const textElement = button.element.querySelector('.mtrl-button-text')
134
+ expect(textElement.textContent).toBe(newText)
135
+ })
136
+
137
+ test('should allow updating icon', () => {
138
+ const button = createButton()
139
+
140
+ const iconSvg = '<svg><path d="M10 10"></path></svg>'
141
+ button.setIcon(iconSvg)
142
+
143
+ const iconElement = button.element.querySelector('.mtrl-button-icon')
144
+ expect(iconElement).toBeDefined()
145
+ expect(iconElement.innerHTML).toBe(iconSvg)
146
+ })
147
+
148
+ test('should properly clean up resources', () => {
149
+ const button = createButton()
150
+ const parentElement = document.createElement('div')
151
+ parentElement.appendChild(button.element)
152
+
153
+ // Destroy should remove the element and clean up resources
154
+ button.destroy()
155
+
156
+ expect(parentElement.children.length).toBe(0)
157
+ })
158
+ })
@@ -0,0 +1,141 @@
1
+ // test/core/state/emitter.test.js
2
+ import { describe, test, expect, mock } from 'bun:test'
3
+ import { createEmitter } from '../../src/core/state/emitter'
4
+
5
+ describe('Event Emitter', () => {
6
+ test('should create an emitter with expected methods', () => {
7
+ const emitter = createEmitter()
8
+ expect(emitter).toBeDefined()
9
+ expect(emitter.on).toBeInstanceOf(Function)
10
+ expect(emitter.off).toBeInstanceOf(Function)
11
+ expect(emitter.emit).toBeInstanceOf(Function)
12
+ expect(emitter.clear).toBeInstanceOf(Function)
13
+ })
14
+
15
+ test('should register event handlers with on()', () => {
16
+ const emitter = createEmitter()
17
+ const handler = mock(() => {})
18
+
19
+ const unsubscribe = emitter.on('test', handler)
20
+
21
+ expect(unsubscribe).toBeInstanceOf(Function)
22
+ })
23
+
24
+ test('should invoke handlers when event is emitted', () => {
25
+ const emitter = createEmitter()
26
+ const handler1 = mock(() => {})
27
+ const handler2 = mock(() => {})
28
+ const eventData = { foo: 'bar' }
29
+
30
+ emitter.on('test', handler1)
31
+ emitter.on('test', handler2)
32
+
33
+ emitter.emit('test', eventData)
34
+
35
+ expect(handler1).toHaveBeenCalledTimes(1)
36
+ expect(handler1).toHaveBeenCalledWith(eventData)
37
+
38
+ expect(handler2).toHaveBeenCalledTimes(1)
39
+ expect(handler2).toHaveBeenCalledWith(eventData)
40
+ })
41
+
42
+ test('should remove handlers with off()', () => {
43
+ const emitter = createEmitter()
44
+ const handler = mock(() => {})
45
+
46
+ emitter.on('test', handler)
47
+ emitter.off('test', handler)
48
+
49
+ emitter.emit('test')
50
+
51
+ expect(handler).not.toHaveBeenCalled()
52
+ })
53
+
54
+ test('should remove specific handlers while keeping others', () => {
55
+ const emitter = createEmitter()
56
+ const handler1 = mock(() => {})
57
+ const handler2 = mock(() => {})
58
+
59
+ emitter.on('test', handler1)
60
+ emitter.on('test', handler2)
61
+
62
+ emitter.off('test', handler1)
63
+
64
+ emitter.emit('test')
65
+
66
+ expect(handler1).not.toHaveBeenCalled()
67
+ expect(handler2).toHaveBeenCalledTimes(1)
68
+ })
69
+
70
+ test('should unsubscribe handlers using the returned function', () => {
71
+ const emitter = createEmitter()
72
+ const handler = mock(() => {})
73
+
74
+ const unsubscribe = emitter.on('test', handler)
75
+ unsubscribe()
76
+
77
+ emitter.emit('test')
78
+
79
+ expect(handler).not.toHaveBeenCalled()
80
+ })
81
+
82
+ test('should support multiple event types', () => {
83
+ const emitter = createEmitter()
84
+ const handler1 = mock(() => {})
85
+ const handler2 = mock(() => {})
86
+
87
+ emitter.on('event1', handler1)
88
+ emitter.on('event2', handler2)
89
+
90
+ emitter.emit('event1')
91
+
92
+ expect(handler1).toHaveBeenCalledTimes(1)
93
+ expect(handler2).not.toHaveBeenCalled()
94
+
95
+ emitter.emit('event2')
96
+
97
+ expect(handler1).toHaveBeenCalledTimes(1)
98
+ expect(handler2).toHaveBeenCalledTimes(1)
99
+ })
100
+
101
+ test('should clear all event handlers', () => {
102
+ const emitter = createEmitter()
103
+ const handler1 = mock(() => {})
104
+ const handler2 = mock(() => {})
105
+
106
+ emitter.on('event1', handler1)
107
+ emitter.on('event2', handler2)
108
+
109
+ emitter.clear()
110
+
111
+ emitter.emit('event1')
112
+ emitter.emit('event2')
113
+
114
+ expect(handler1).not.toHaveBeenCalled()
115
+ expect(handler2).not.toHaveBeenCalled()
116
+ })
117
+
118
+ test('should pass multiple arguments to handlers', () => {
119
+ const emitter = createEmitter()
120
+ const handler = mock(() => {})
121
+
122
+ emitter.on('test', handler)
123
+
124
+ const arg1 = { id: 1 }
125
+ const arg2 = 'string'
126
+ const arg3 = [1, 2, 3]
127
+
128
+ emitter.emit('test', arg1, arg2, arg3)
129
+
130
+ expect(handler).toHaveBeenCalledWith(arg1, arg2, arg3)
131
+ })
132
+
133
+ test('should do nothing when emitting event with no handlers', () => {
134
+ const emitter = createEmitter()
135
+
136
+ // This should not throw
137
+ expect(() => {
138
+ emitter.emit('nonexistent')
139
+ }).not.toThrow()
140
+ })
141
+ })
@@ -0,0 +1,165 @@
1
+ // test/core/build/ripple.test.js
2
+ import { describe, test, expect, mock, spyOn } from 'bun:test'
3
+ import { createRipple } from '../../src/core/build/ripple'
4
+ import '../setup'
5
+
6
+ describe('Ripple Effect', () => {
7
+ test('should create a ripple controller', () => {
8
+ const ripple = createRipple()
9
+ expect(ripple).toBeDefined()
10
+ expect(ripple.mount).toBeInstanceOf(Function)
11
+ expect(ripple.unmount).toBeInstanceOf(Function)
12
+ })
13
+
14
+ test('should mount ripple effect to an element', () => {
15
+ const ripple = createRipple()
16
+ const element = document.createElement('div')
17
+
18
+ // Element should start with static position
19
+ element.style.position = 'static'
20
+
21
+ ripple.mount(element)
22
+
23
+ // Position should now be relative for proper ripple positioning
24
+ expect(element.style.position).toBe('relative')
25
+ expect(element.style.overflow).toBe('hidden')
26
+
27
+ // Should have added mousedown event listener
28
+ expect(element.__handlers).toBeDefined()
29
+ expect(element.__handlers.mousedown).toBeDefined()
30
+ })
31
+
32
+ test('should not fail when mounting to a null element', () => {
33
+ const ripple = createRipple()
34
+ expect(() => ripple.mount(null)).not.toThrow()
35
+ })
36
+
37
+ test('should create ripple element on mousedown', () => {
38
+ const ripple = createRipple()
39
+ const element = document.createElement('div')
40
+
41
+ // Spy on appendChild to verify ripple element creation
42
+ const appendChildSpy = spyOn(element, 'appendChild')
43
+
44
+ ripple.mount(element)
45
+
46
+ // Simulate mousedown event
47
+ const mouseEvent = {
48
+ type: 'mousedown',
49
+ offsetX: 10,
50
+ offsetY: 20,
51
+ target: element
52
+ }
53
+ element.__handlers.mousedown[0](mouseEvent)
54
+
55
+ // Should have created and appended a ripple element
56
+ expect(appendChildSpy).toHaveBeenCalled()
57
+ expect(appendChildSpy.mock.calls[0][0].className).toBe('ripple')
58
+ })
59
+
60
+ test('should add document cleanup event listeners', () => {
61
+ const ripple = createRipple()
62
+ const element = document.createElement('div')
63
+
64
+ // Mock document event listeners
65
+ const docAddEventListener = mock(() => {})
66
+ const originalDocAddEventListener = document.addEventListener
67
+ document.addEventListener = docAddEventListener
68
+
69
+ ripple.mount(element)
70
+
71
+ // Simulate mousedown event
72
+ const mouseEvent = {
73
+ type: 'mousedown',
74
+ offsetX: 10,
75
+ offsetY: 20,
76
+ target: element
77
+ }
78
+ element.__handlers.mousedown[0](mouseEvent)
79
+
80
+ // Should have added mouseup and mouseleave event listeners to document
81
+ expect(docAddEventListener).toHaveBeenCalledTimes(2)
82
+ expect(docAddEventListener.mock.calls[0][0]).toBe('mouseup')
83
+ expect(docAddEventListener.mock.calls[1][0]).toBe('mouseleave')
84
+
85
+ // Restore original
86
+ document.addEventListener = originalDocAddEventListener
87
+ })
88
+
89
+ test('should remove ripple elements on unmount', () => {
90
+ const ripple = createRipple()
91
+ const element = document.createElement('div')
92
+
93
+ // Add a few ripple elements
94
+ const ripple1 = document.createElement('div')
95
+ ripple1.className = 'ripple'
96
+ const ripple2 = document.createElement('div')
97
+ ripple2.className = 'ripple'
98
+
99
+ element.appendChild(ripple1)
100
+ element.appendChild(ripple2)
101
+
102
+ // Mock the querySelectorAll and forEach methods
103
+ element.querySelectorAll = (selector) => {
104
+ if (selector === '.ripple') {
105
+ return [ripple1, ripple2]
106
+ }
107
+ return []
108
+ }
109
+
110
+ const removeSpy1 = spyOn(ripple1, 'remove')
111
+ const removeSpy2 = spyOn(ripple2, 'remove')
112
+
113
+ ripple.unmount(element)
114
+
115
+ // Should have removed both ripple elements
116
+ expect(removeSpy1).toHaveBeenCalled()
117
+ expect(removeSpy2).toHaveBeenCalled()
118
+ })
119
+
120
+ // test('should handle custom config options', () => {
121
+ // const customConfig = {
122
+ // duration: 500,
123
+ // timing: 'ease-out',
124
+ // opacity: ['0.8', '0.2']
125
+ // }
126
+
127
+ // const ripple = createRipple(customConfig)
128
+ // const element = document.createElement('div')
129
+
130
+ // // Spy on appendChild to capture the ripple element
131
+ // let capturedRipple
132
+ // const originalAppendChild = element.appendChild
133
+ // element.appendChild = (child) => {
134
+ // capturedRipple = child
135
+ // return originalAppendChild.call(element, child)
136
+ // }
137
+
138
+ // ripple.mount(element)
139
+
140
+ // // Simulate mousedown event
141
+ // const mouseEvent = {
142
+ // type: 'mousedown',
143
+ // offsetX: 10,
144
+ // offsetY: 20,
145
+ // target: element
146
+ // }
147
+ // element.__handlers.mousedown[0](mouseEvent)
148
+
149
+ // // Verify custom config was applied
150
+ // expect(capturedRipple.style.transition).toContain(`${customConfig.duration}ms`)
151
+ // expect(capturedRipple.style.transition).toContain(customConfig.timing)
152
+ // expect(capturedRipple.style.opacity).toBe(customConfig.opacity[0])
153
+
154
+ // // Force reflow simulation
155
+ // capturedRipple.offsetHeight
156
+
157
+ // // Check end opacity is applied after animation
158
+ // expect(capturedRipple.style.opacity).toBe(customConfig.opacity[1])
159
+ // })
160
+
161
+ test('should not fail when unmounting a null element', () => {
162
+ const ripple = createRipple()
163
+ expect(() => ripple.unmount(null)).not.toThrow()
164
+ })
165
+ })