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 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, ES6-focused JavaScript UI component library.
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
- - ES6+ focused codebase
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 JavaScript patterns and component design.
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
- ```javascript
80
- // src/components/mycomponent/index.js
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/mycomponent.js
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
- // etc...
87
-
88
- export const createMyComponent = (options = {}) => {
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/views/components/` for your component.
112
- 3. Add the route in `src/client/core/navigation.js` of the mtrl.app repository.
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
- - Use ES6+ features but maintain browser compatibility
121
- - Follow functional programming principles when possible
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
- - Write JSDoc comments for all public functions
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
- ```javascript
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
- For any new feature or component:
183
+ Documentation is crucial for this project:
162
184
 
163
- - Add JSDoc comments for API methods
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A functional TypeScript/JavaScript component library with composable architecture based on Material Design 3",
5
5
  "author": "floor",
6
6
  "license": "MIT License",
@@ -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
- const label = document.createElement('span');
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
  }
@@ -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, boolean | ((context: any, event: Event) => boolean)>;
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]: EventListener;
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
- // Handle content
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 normalizedClasses = normalizeClasses(className);
119
- if (normalizedClasses.length) {
120
- element.classList.add(...normalizedClasses);
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
- Object.entries(data).forEach(([key, value]) => {
126
- element.dataset[key] = value;
127
- });
141
+ // Handle data attributes directly
142
+ for (const key in data) {
143
+ element.dataset[key] = data[key];
144
+ }
128
145
 
129
- // Handle all other attributes
146
+ // Handle regular attributes
130
147
  const allAttrs = { ...attrs, ...rest };
131
- Object.entries(allAttrs).forEach(([key, value]) => {
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 if context has emit method or is a component with on method
153
+ // Handle event forwarding
141
154
  if (forwardEvents && (context?.emit || context?.on)) {
142
- Object.entries(forwardEvents).forEach(([nativeEvent, eventConfig]) => {
143
- // Create a wrapper handler function to evaluate condition and forward event
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
- // If it's a function, call with component context and event
151
- shouldForward = eventConfig({ ...context, element }, event);
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
- // This is a component with on method but no emit method
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
- container.appendChild(element);
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
- if (element.__eventHandlers) {
204
- Object.entries(element.__eventHandlers).forEach(([eventName, handler]) => {
205
- element.removeEventListener(eventName, handler);
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
- element.appendChild(content);
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