mtrl 0.3.2 → 0.3.3
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.
- package/CONTRIBUTING.md +56 -17
- package/package.json +1 -1
- package/src/components/navigation/nav-item.ts +13 -2
- package/src/core/dom/create.ts +57 -51
- package/src/styles/themes/_autumn.scss +3 -0
package/CONTRIBUTING.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Contributing to mtrl
|
|
2
2
|
|
|
3
|
-
Thank you for your interest in contributing to mtrl! This document provides guidelines and instructions for contributing to this lightweight,
|
|
3
|
+
Thank you for your interest in contributing to mtrl! This document provides guidelines and instructions for contributing to this lightweight, TypeScript-focused UI component library.
|
|
4
4
|
|
|
5
5
|
## Why Contribute?
|
|
6
6
|
|
|
7
7
|
mtrl aims to be a modern, flexible UI component library with:
|
|
8
8
|
|
|
9
9
|
- Zero dependencies (except Bun for development)
|
|
10
|
-
-
|
|
10
|
+
- TypeScript-first codebase
|
|
11
11
|
- Lightweight, tree-shakable components
|
|
12
12
|
- Simple and extensible API
|
|
13
13
|
- Excellent documentation
|
|
14
14
|
|
|
15
|
-
By contributing to mtrl, you'll help create a lean alternative to heavier frameworks while gaining experience with modern
|
|
15
|
+
By contributing to mtrl, you'll help create a lean alternative to heavier frameworks while gaining experience with modern TypeScript patterns and component design.
|
|
16
16
|
|
|
17
17
|
## Getting Started
|
|
18
18
|
|
|
@@ -76,16 +76,29 @@ mtrl uses a separate repository called mtrl.app (https://mtrl.app) for showcasin
|
|
|
76
76
|
|
|
77
77
|
mtrl components follow a consistent pattern:
|
|
78
78
|
|
|
79
|
-
```
|
|
80
|
-
// src/components/mycomponent/index.
|
|
79
|
+
```typescript
|
|
80
|
+
// src/components/mycomponent/index.ts
|
|
81
81
|
export { createMyComponent } from './mycomponent';
|
|
82
|
+
export type { MyComponentOptions } from './types';
|
|
82
83
|
|
|
83
|
-
// src/components/mycomponent/
|
|
84
|
+
// src/components/mycomponent/types.ts
|
|
85
|
+
export interface MyComponentOptions {
|
|
86
|
+
text?: string;
|
|
87
|
+
onClick?: (event: MouseEvent) => void;
|
|
88
|
+
// other options...
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/components/mycomponent/mycomponent.ts
|
|
84
92
|
import { createElement } from '../../core/dom/create';
|
|
85
93
|
import { createLifecycle } from '../../core/state/lifecycle';
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
import type { MyComponentOptions } from './types';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a new MyComponent instance
|
|
98
|
+
* @param options - Configuration options for MyComponent
|
|
99
|
+
* @returns The MyComponent instance
|
|
100
|
+
*/
|
|
101
|
+
export const createMyComponent = (options: MyComponentOptions = {}) => {
|
|
89
102
|
// Create DOM elements
|
|
90
103
|
const element = createElement({...});
|
|
91
104
|
|
|
@@ -108,21 +121,30 @@ export const createMyComponent = (options = {}) => {
|
|
|
108
121
|
The mtrl.app showcase application is the best way to develop and test your components:
|
|
109
122
|
|
|
110
123
|
1. Clone the mtrl.app repository alongside your mtrl clone.
|
|
111
|
-
2. Create a new view file in `src/client/
|
|
112
|
-
3. Add the route in `src/client/core/navigation.
|
|
124
|
+
2. Create a new view file in `src/client/content/components/` for your component.
|
|
125
|
+
3. Add the route in `src/client/core/navigation.ts` of the mtrl.app repository.
|
|
113
126
|
4. Implement different variants and states for testing.
|
|
114
127
|
5. Run the showcase server with `bun run dev` in the mtrl.app directory.
|
|
115
128
|
|
|
116
129
|
This separation of the library code (mtrl) and the showcase app (mtrl.app) keeps the core library clean while providing a rich development environment.
|
|
117
130
|
|
|
131
|
+
### TypeScript Standards
|
|
132
|
+
|
|
133
|
+
- Use TypeScript's type system to create clear interfaces and types
|
|
134
|
+
- Export types and interfaces separately from implementations
|
|
135
|
+
- Use strict typing and avoid `any` when possible
|
|
136
|
+
- Prefer interfaces for public APIs and type aliases for complex types
|
|
137
|
+
- Add proper return types to all functions
|
|
138
|
+
|
|
118
139
|
### Coding Standards
|
|
119
140
|
|
|
120
|
-
-
|
|
121
|
-
-
|
|
141
|
+
- Add file path as a comment on the first line of each file
|
|
142
|
+
- Use functional programming principles when possible
|
|
122
143
|
- Use consistent naming conventions:
|
|
123
144
|
- Factory functions should be named `createXyz`
|
|
124
145
|
- Utilities should use clear, descriptive names
|
|
125
|
-
-
|
|
146
|
+
- Interfaces should be named in PascalCase (e.g., `ButtonOptions`)
|
|
147
|
+
- Write TypeDoc comments for all public functions and types
|
|
126
148
|
|
|
127
149
|
### CSS/SCSS Guidelines
|
|
128
150
|
|
|
@@ -143,7 +165,7 @@ This separation of the library code (mtrl) and the showcase app (mtrl.app) keeps
|
|
|
143
165
|
|
|
144
166
|
Please add appropriate tests for your changes:
|
|
145
167
|
|
|
146
|
-
```
|
|
168
|
+
```typescript
|
|
147
169
|
// Example test structure
|
|
148
170
|
describe('myComponent', () => {
|
|
149
171
|
it('should render correctly', () => {
|
|
@@ -158,12 +180,29 @@ describe('myComponent', () => {
|
|
|
158
180
|
|
|
159
181
|
## Documentation
|
|
160
182
|
|
|
161
|
-
|
|
183
|
+
Documentation is crucial for this project:
|
|
162
184
|
|
|
163
|
-
- Add
|
|
185
|
+
- Add TypeDoc comments for all public API methods and types
|
|
186
|
+
- Comment the file path at the top of each file
|
|
164
187
|
- Update the component's README.md (if applicable)
|
|
165
188
|
- Consider adding example code in the playground
|
|
166
189
|
|
|
190
|
+
Example of proper TypeDoc:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
/**
|
|
194
|
+
* Creates a button element with specified options
|
|
195
|
+
*
|
|
196
|
+
* @param options - The button configuration options
|
|
197
|
+
* @returns A button component instance
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const button = createButton({ text: 'Click me', variant: 'primary' });
|
|
201
|
+
* document.body.appendChild(button.element);
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
```
|
|
205
|
+
|
|
167
206
|
## Community and Communication
|
|
168
207
|
|
|
169
208
|
- Submit issues for bugs or feature requests
|
package/package.json
CHANGED
|
@@ -107,11 +107,22 @@ export const createNavItem = (
|
|
|
107
107
|
itemElement.appendChild(icon);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// Add label if provided
|
|
110
|
+
// Add label if provided - CONVERTED TO ANCHOR FOR SEO
|
|
111
111
|
if (config.label) {
|
|
112
|
-
|
|
112
|
+
// Create anchor instead of span for the label
|
|
113
|
+
const label = document.createElement('a');
|
|
113
114
|
label.className = `${prefix}-${NavClass.LABEL}`;
|
|
114
115
|
label.textContent = config.label;
|
|
116
|
+
label.href = `/${config.id}`; // Create path based on ID
|
|
117
|
+
|
|
118
|
+
// Ensure the anchor looks the same as a span would
|
|
119
|
+
label.style.textDecoration = 'none';
|
|
120
|
+
label.style.color = 'inherit';
|
|
121
|
+
label.style.pointerEvents = 'none'; // Let the button handle click events
|
|
122
|
+
|
|
123
|
+
// Add SEO attributes
|
|
124
|
+
label.setAttribute('title', config.label);
|
|
125
|
+
|
|
115
126
|
itemElement.appendChild(label);
|
|
116
127
|
itemElement.setAttribute('aria-label', config.label);
|
|
117
128
|
}
|
package/src/core/dom/create.ts
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import { setAttributes } from './attributes';
|
|
8
8
|
import { normalizeClasses } from '../utils';
|
|
9
|
+
import { PREFIX } from '../config';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Event handler function type
|
|
13
|
+
*/
|
|
14
|
+
export type EventHandler = (event: Event) => void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Event condition type - either a boolean or a function that returns a boolean
|
|
18
|
+
*/
|
|
19
|
+
export type EventCondition = boolean | ((context: any, event: Event) => boolean);
|
|
9
20
|
|
|
10
21
|
/**
|
|
11
22
|
* Options for element creation
|
|
@@ -59,7 +70,7 @@ export interface CreateElementOptions {
|
|
|
59
70
|
/**
|
|
60
71
|
* Events to forward when component has emit method
|
|
61
72
|
*/
|
|
62
|
-
forwardEvents?: Record<string,
|
|
73
|
+
forwardEvents?: Record<string, EventCondition>;
|
|
63
74
|
|
|
64
75
|
/**
|
|
65
76
|
* Callback after element creation
|
|
@@ -81,9 +92,12 @@ export interface CreateElementOptions {
|
|
|
81
92
|
* Event handler storage to facilitate cleanup
|
|
82
93
|
*/
|
|
83
94
|
export interface EventHandlerStorage {
|
|
84
|
-
[eventName: string]:
|
|
95
|
+
[eventName: string]: EventHandler;
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
// Constant for prefix with dash
|
|
99
|
+
const PREFIX_WITH_DASH = `${PREFIX}-`;
|
|
100
|
+
|
|
87
101
|
/**
|
|
88
102
|
* Creates a DOM element with the specified options
|
|
89
103
|
*
|
|
@@ -108,89 +122,78 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
|
|
|
108
122
|
|
|
109
123
|
const element = document.createElement(tag);
|
|
110
124
|
|
|
111
|
-
//
|
|
125
|
+
// Apply basic properties
|
|
112
126
|
if (html) element.innerHTML = html;
|
|
113
127
|
if (text) element.textContent = text;
|
|
114
128
|
if (id) element.id = id;
|
|
115
129
|
|
|
116
130
|
// Handle classes
|
|
117
131
|
if (className) {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
132
|
+
const classes = normalizeClasses(className);
|
|
133
|
+
if (classes.length) {
|
|
134
|
+
// Apply prefix to classes in a single operation
|
|
135
|
+
element.classList.add(...classes.map(cls =>
|
|
136
|
+
cls && !cls.startsWith(PREFIX_WITH_DASH) ? PREFIX_WITH_DASH + cls : cls
|
|
137
|
+
).filter(Boolean));
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
// Handle data attributes
|
|
125
|
-
|
|
126
|
-
element.dataset[key] =
|
|
127
|
-
}
|
|
141
|
+
// Handle data attributes directly
|
|
142
|
+
for (const key in data) {
|
|
143
|
+
element.dataset[key] = data[key];
|
|
144
|
+
}
|
|
128
145
|
|
|
129
|
-
// Handle
|
|
146
|
+
// Handle regular attributes
|
|
130
147
|
const allAttrs = { ...attrs, ...rest };
|
|
131
|
-
|
|
148
|
+
for (const key in allAttrs) {
|
|
149
|
+
const value = allAttrs[key];
|
|
132
150
|
if (value != null) element.setAttribute(key, String(value));
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Initialize event handler storage if not present
|
|
136
|
-
if (!element.__eventHandlers) {
|
|
137
|
-
element.__eventHandlers = {};
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
// Handle event forwarding
|
|
153
|
+
// Handle event forwarding
|
|
141
154
|
if (forwardEvents && (context?.emit || context?.on)) {
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
element.__eventHandlers = {};
|
|
156
|
+
|
|
157
|
+
for (const nativeEvent in forwardEvents) {
|
|
158
|
+
const eventConfig = forwardEvents[nativeEvent];
|
|
159
|
+
|
|
144
160
|
const handler = (event: Event) => {
|
|
145
|
-
// Determine if the event should be forwarded
|
|
146
161
|
let shouldForward = true;
|
|
147
162
|
|
|
148
163
|
if (typeof eventConfig === 'function') {
|
|
149
164
|
try {
|
|
150
|
-
//
|
|
151
|
-
|
|
165
|
+
// Create a lightweight context clone
|
|
166
|
+
const ctxWithElement = { ...context, element };
|
|
167
|
+
shouldForward = eventConfig(ctxWithElement, event);
|
|
152
168
|
} catch (error) {
|
|
153
169
|
console.warn(`Error in event condition for ${nativeEvent}:`, error);
|
|
154
170
|
shouldForward = false;
|
|
155
171
|
}
|
|
156
172
|
} else {
|
|
157
|
-
// If it's a boolean, use directly
|
|
158
173
|
shouldForward = Boolean(eventConfig);
|
|
159
174
|
}
|
|
160
175
|
|
|
161
|
-
// Forward the event if condition passes
|
|
162
176
|
if (shouldForward) {
|
|
163
177
|
if (context.emit) {
|
|
164
178
|
context.emit(nativeEvent, { event, element, originalEvent: event });
|
|
165
179
|
} else if (context.on) {
|
|
166
|
-
|
|
167
|
-
// Dispatch a custom event that can be listened to
|
|
168
|
-
const customEvent = new CustomEvent(nativeEvent, {
|
|
180
|
+
element.dispatchEvent(new CustomEvent(nativeEvent, {
|
|
169
181
|
detail: { event, element, originalEvent: event },
|
|
170
182
|
bubbles: true,
|
|
171
183
|
cancelable: true
|
|
172
|
-
});
|
|
173
|
-
element.dispatchEvent(customEvent);
|
|
184
|
+
}));
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
};
|
|
177
188
|
|
|
178
|
-
// Store the handler for future removal
|
|
179
189
|
element.__eventHandlers[nativeEvent] = handler;
|
|
180
|
-
|
|
181
|
-
// Add the actual event listener
|
|
182
190
|
element.addEventListener(nativeEvent, handler);
|
|
183
|
-
}
|
|
191
|
+
}
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
// Append to container if provided
|
|
187
|
-
if (container)
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (typeof onCreate === 'function') {
|
|
192
|
-
onCreate(element, context);
|
|
193
|
-
}
|
|
195
|
+
if (container) container.appendChild(element);
|
|
196
|
+
if (onCreate) onCreate(element, context);
|
|
194
197
|
|
|
195
198
|
return element;
|
|
196
199
|
};
|
|
@@ -200,10 +203,11 @@ export const createElement = (options: CreateElementOptions = {}): HTMLElement =
|
|
|
200
203
|
* @param element - Element to cleanup
|
|
201
204
|
*/
|
|
202
205
|
export const removeEventHandlers = (element: HTMLElement): void => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
const handlers = element.__eventHandlers;
|
|
207
|
+
if (handlers) {
|
|
208
|
+
for (const event in handlers) {
|
|
209
|
+
element.removeEventListener(event, handlers[event]);
|
|
210
|
+
}
|
|
207
211
|
delete element.__eventHandlers;
|
|
208
212
|
}
|
|
209
213
|
};
|
|
@@ -228,7 +232,9 @@ export const withClasses = (...classes: (string | string[])[]) =>
|
|
|
228
232
|
(element: HTMLElement): HTMLElement => {
|
|
229
233
|
const normalizedClasses = normalizeClasses(...classes);
|
|
230
234
|
if (normalizedClasses.length) {
|
|
231
|
-
element.classList.add(...normalizedClasses
|
|
235
|
+
element.classList.add(...normalizedClasses.map(cls =>
|
|
236
|
+
cls && !cls.startsWith(PREFIX_WITH_DASH) ? PREFIX_WITH_DASH + cls : cls
|
|
237
|
+
).filter(Boolean));
|
|
232
238
|
}
|
|
233
239
|
return element;
|
|
234
240
|
};
|
|
@@ -240,17 +246,17 @@ export const withClasses = (...classes: (string | string[])[]) =>
|
|
|
240
246
|
*/
|
|
241
247
|
export const withContent = (content: Node | string) =>
|
|
242
248
|
(element: HTMLElement): HTMLElement => {
|
|
243
|
-
if (content instanceof Node)
|
|
244
|
-
|
|
245
|
-
} else {
|
|
246
|
-
element.textContent = content;
|
|
247
|
-
}
|
|
249
|
+
if (content instanceof Node) element.appendChild(content);
|
|
250
|
+
else element.textContent = content;
|
|
248
251
|
return element;
|
|
249
252
|
};
|
|
250
253
|
|
|
251
254
|
// Extend HTMLElement interface to add eventHandlers property
|
|
252
255
|
declare global {
|
|
253
256
|
interface HTMLElement {
|
|
257
|
+
/**
|
|
258
|
+
* Storage for event handlers to enable cleanup
|
|
259
|
+
*/
|
|
254
260
|
__eventHandlers?: EventHandlerStorage;
|
|
255
261
|
}
|
|
256
262
|
}
|
|
@@ -58,6 +58,9 @@
|
|
|
58
58
|
--#{$prefix}-sys-color-on-info: #003060;
|
|
59
59
|
--#{$prefix}-sys-color-on-info-rgb: 0, 48, 96;
|
|
60
60
|
|
|
61
|
+
// Include status colors for light theme
|
|
62
|
+
@include status-colors-light();
|
|
63
|
+
|
|
61
64
|
&[data-theme-mode="dark"] {
|
|
62
65
|
// Key colors
|
|
63
66
|
--#{$prefix}-sys-color-primary: #DDB995; // Softer brown
|