lapikit 0.7.0 → 0.7.1

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,58 +1,117 @@
1
+ import { DEV } from 'esm-env';
2
+ import { makeComponentProps } from '../html-mapped';
3
+ const endsWithSeparator = /[^a-zA-Z0-9]$/;
4
+ const startsWithSeparator = /^[^a-zA-Z0-9]/;
5
+ const whitespace = /\s/;
6
+ const whitespaces = /\s+/;
7
+ const propertyCache = new Map();
8
+ function hasKeys(value) {
9
+ if (!value || typeof value !== 'object')
10
+ return false;
11
+ for (const key in value) {
12
+ if (Object.hasOwn(value, key))
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ /**
18
+ * Adds the classes of a value (one or several, separated by spaces) to the set.
19
+ * The set keeps the first position of each class and drops the duplicates.
20
+ */
21
+ function addClasses(classes, value) {
22
+ if (!whitespace.test(value)) {
23
+ if (value)
24
+ classes.add(value);
25
+ return;
26
+ }
27
+ for (const name of value.split(whitespaces)) {
28
+ if (name)
29
+ classes.add(name);
30
+ }
31
+ }
32
+ /**
33
+ * Adds a class value with the rules of clsx (class on an element): a string, an array (nested too),
34
+ * or an object whose keys are added when their value is truthy. Joining an array would give "a,b"
35
+ */
36
+ function addClassValue(classes, value) {
37
+ if (typeof value === 'string')
38
+ addClasses(classes, value);
39
+ else if (Array.isArray(value)) {
40
+ for (const item of value)
41
+ addClassValue(classes, item);
42
+ }
43
+ else if (value && typeof value === 'object') {
44
+ for (const key in value) {
45
+ if (value[key])
46
+ addClasses(classes, key);
47
+ }
48
+ }
49
+ }
50
+ /**
51
+ * The classes of the s-class_xxx directives (class:xxx on a kit tag):
52
+ * - true gives the name (class:active -> active);
53
+ * - a number or a text gives the name and the value, joined by a dash (class:gap={4} -> gap-4),
54
+ * without a dash when the name ends or the value starts with a separator (- _ : / .):
55
+ * s-class_gap-={4}, s-class_variant="-primary" and s-class_state=":active" keep working;
56
+ * - false, null, undefined and '' give nothing.
57
+ */
58
+ export function directiveClasses(classProps) {
59
+ const classes = [];
60
+ if (!classProps)
61
+ return classes;
62
+ for (const key in classProps) {
63
+ const value = classProps[key];
64
+ // 8 = 's-class_'.length
65
+ const base = key.slice(8);
66
+ if (value === true)
67
+ classes.push(base);
68
+ else if ((typeof value === 'string' && value) || typeof value === 'number') {
69
+ const text = String(value);
70
+ const dash = endsWithSeparator.test(base) || startsWithSeparator.test(text) ? '' : '-';
71
+ classes.push(`${base}${dash}${text}`);
72
+ }
73
+ }
74
+ return classes;
75
+ }
1
76
  /**
2
77
  * useClassName - Utility to compute class names for a component.
3
78
  * @param baseClass - The base class name for the component.
4
- * @param className - Additional class names as a string.
79
+ * @param className - Additional class names: a string or an array of strings.
5
80
  * @param sClass - The s-class property which can be a string, array, or object.
6
81
  * @param classProps - An object containing s-class_xxx directives.
7
- * @returns A computed class string.
82
+ * @returns A computed class string: no extra spaces, no duplicates, in the order of the inputs.
8
83
  */
9
84
  export function useClassName({ baseClass = '', className, sClass, classProps } = {}) {
10
- const classes = [];
11
- if (baseClass) {
12
- classes.push(baseClass);
85
+ if (!sClass && !className && !hasKeys(classProps))
86
+ return baseClass;
87
+ const classes = new Set();
88
+ if (baseClass)
89
+ addClasses(classes, baseClass);
90
+ if (typeof sClass === 'string') {
91
+ addClasses(classes, sClass);
13
92
  }
14
- if (typeof sClass === 'string' && sClass) {
15
- classes.push(sClass);
16
- }
17
- if (Array.isArray(sClass)) {
93
+ else if (Array.isArray(sClass)) {
18
94
  for (const value of sClass) {
19
- if (typeof value === 'string' && value) {
20
- classes.push(value);
21
- }
95
+ if (typeof value === 'string')
96
+ addClasses(classes, value);
22
97
  }
23
98
  }
24
- if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) {
25
- const entries = Object.entries(sClass);
26
- if (entries.length > 0) {
27
- for (const [key, value] of entries) {
28
- if (value === true) {
29
- classes.push(key);
30
- }
31
- else if (typeof value === 'string' && value) {
32
- classes.push(value);
33
- }
34
- }
99
+ else if (sClass && typeof sClass === 'object') {
100
+ for (const key in sClass) {
101
+ const value = sClass[key];
102
+ if (value === true)
103
+ addClasses(classes, key);
104
+ else if (typeof value === 'string')
105
+ addClasses(classes, value);
35
106
  }
36
107
  }
37
108
  if (classProps) {
38
- const entries = Object.entries(classProps);
39
- if (entries.length > 0) {
40
- for (const [key, value] of entries) {
41
- // Use slice instead of replace for better performance (8 = 's-class_'.length)
42
- const base = key.slice(8);
43
- if (value === true) {
44
- classes.push(base);
45
- }
46
- else if (typeof value === 'string' && value) {
47
- classes.push(`${base}${value}`);
48
- }
49
- }
50
- }
51
- }
52
- if (className) {
53
- classes.push(className);
109
+ for (const name of directiveClasses(classProps))
110
+ addClasses(classes, name);
54
111
  }
55
- return classes.filter(Boolean).join(' ');
112
+ if (className)
113
+ addClassValue(classes, className);
114
+ return [...classes].join(' ');
56
115
  }
57
116
  /**
58
117
  * useIsInteractive - Utility to determine whether a component should behave as interactive:
@@ -71,40 +130,72 @@ export function useIsInteractive(props, tag, interactiveTags, interactive = fals
71
130
  interactiveEventKeys.some((key) => typeof props[key] === 'function'));
72
131
  }
73
132
  /**
74
- * useStyles - Utility to compute style declarations for a component (optimized pure function).
75
- * @param styleAttr - Inline style attribute as a string.
76
- * @param sStyle - The s-style property which is an object of style key-value pairs.
133
+ * The CSS name of a property: backgroundColor gives background-color.
134
+ * A custom property (--x) or a name already in kebab-case is kept as it is.
135
+ */
136
+ function toCssProperty(name) {
137
+ if (name.startsWith('--') || !/[A-Z]/.test(name))
138
+ return name;
139
+ let property = propertyCache.get(name);
140
+ if (property === undefined) {
141
+ property = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
142
+ // Vendor prefixes: WebkitMask gives -webkit-mask
143
+ if (/^(webkit|moz|ms)-/.test(property))
144
+ property = `-${property}`;
145
+ propertyCache.set(name, property);
146
+ }
147
+ return property;
148
+ }
149
+ /**
150
+ * The declaration of a style value, or undefined to skip it:
151
+ * - a number (0 included) or a non empty string is kept;
152
+ * - null, undefined, false, true and '' are skipped (true is not a CSS value);
153
+ * - a value holding ; { or } is refused: it could add other declarations (CSS injection).
154
+ */
155
+ function toDeclaration(name, value) {
156
+ if (typeof value === 'number') {
157
+ return Number.isFinite(value) ? `${toCssProperty(name)}: ${value}` : undefined;
158
+ }
159
+ if (typeof value !== 'string' || !value)
160
+ return undefined;
161
+ if (/[;{}]/.test(value)) {
162
+ if (DEV) {
163
+ console.warn(`[lapikit] the style value of "${name}" is ignored: it contains ; { or } ("${value}")`);
164
+ }
165
+ return undefined;
166
+ }
167
+ return `${toCssProperty(name)}: ${value}`;
168
+ }
169
+ /**
170
+ * useStyles - Utility to compute style declarations for a component (pure function).
171
+ * @param styleAttr - Inline style attribute as a string (raw CSS, kept as it is).
172
+ * @param sStyle - The s-style property: an object of property / value pairs (camelCase or kebab-case).
77
173
  * @param styleProps - An object containing s-style_xxx directives.
78
- * @returns A computed style string.
174
+ * @returns A computed style string; the style attribute comes last, so it wins.
79
175
  */
80
176
  export function useStyles({ styleAttr, sStyle, styleProps } = {}) {
177
+ // Fast path, the most common case: nothing to merge
178
+ if (!hasKeys(sStyle) && !hasKeys(styleProps))
179
+ return styleAttr ?? '';
81
180
  const styles = [];
82
181
  if (sStyle && typeof sStyle === 'object') {
83
- const entries = Object.entries(sStyle);
84
- if (entries.length > 0) {
85
- for (const [key, value] of entries) {
86
- if (value) {
87
- styles.push(`${key}: ${value}`);
88
- }
89
- }
182
+ for (const key in sStyle) {
183
+ const declaration = toDeclaration(key, sStyle[key]);
184
+ if (declaration)
185
+ styles.push(declaration);
90
186
  }
91
187
  }
92
188
  if (styleProps) {
93
- const entries = Object.entries(styleProps);
94
- if (entries.length > 0) {
95
- for (const [key, value] of entries) {
96
- // Use slice instead of replace for better performance (8 = 's-style_'.length)
97
- const base = key.slice(8);
98
- if (value) {
99
- styles.push(`${base}: ${value}`);
100
- }
101
- }
189
+ for (const key in styleProps) {
190
+ // 8 = 's-style_'.length
191
+ const declaration = toDeclaration(key.slice(8), styleProps[key]);
192
+ if (declaration)
193
+ styles.push(declaration);
102
194
  }
103
195
  }
104
- if (styleAttr) {
196
+ if (styleAttr)
105
197
  styles.push(styleAttr);
106
- }
107
- return styles.filter(Boolean).join('; ');
198
+ return styles.join('; ');
108
199
  }
109
200
  /**
110
201
  * useElevation - Utility to resolve the `elevation` prop into its base/hover/active states.
@@ -125,3 +216,23 @@ export function useElevation(elevation) {
125
216
  active: elevation.active
126
217
  };
127
218
  }
219
+ /**
220
+ * useComponentAttrs - The class, style and other attributes of a labs component, in one call.
221
+ *
222
+ * The class is left to Svelte (clsx): class and s-class follow the same rules as class on an element,
223
+ * a string, an array, or an object whose keys are added when their value is truthy
224
+ * ({ active: isActive } -> active). The s-class_xxx directives (class:xxx on a kit tag) keep their
225
+ * rule: class:gap={4} -> gap-4.
226
+ *
227
+ * @param baseClass - The class of the component ('kit-btn-v2'), '' for none.
228
+ * @param props - The class, s-class, style and s-style props of the component.
229
+ * @param rest - The rest of the props: the s-class_xxx / s-style_xxx directives are taken out of it.
230
+ */
231
+ export function useComponentAttrs(baseClass, { className, sClass, styleAttr, sStyle }, rest) {
232
+ const { classProps, styleProps, restProps } = makeComponentProps(rest);
233
+ return {
234
+ class: [baseClass, sClass, directiveClasses(classProps), className],
235
+ style: useStyles({ styleAttr, sStyle, styleProps }),
236
+ rest: restProps
237
+ };
238
+ }
@@ -0,0 +1,20 @@
1
+ import type { PropValue } from '../@types';
2
+ /**
3
+ * The classes of the s-class_xxx directives (class:xxx on a kit tag):
4
+ * - true gives the name (class:active -> active);
5
+ * - a number or a text gives the name and the value, joined by a dash (class:gap={4} -> gap-4),
6
+ * without a dash when the name ends or the value starts with a separator (- _ : / .):
7
+ * s-class_gap-={4}, s-class_variant="-primary" and s-class_state=":active" keep working;
8
+ * - false, null, undefined and '' give nothing.
9
+ */
10
+ export declare function directiveClasses(classProps: Record<string, PropValue> | undefined): string[];
11
+ /**
12
+ * Adds the classes of a value (one or several, separated by spaces) to the set.
13
+ * The set keeps the first position of each class and drops the duplicates.
14
+ */
15
+ export declare function addClasses(classes: Set<string>, value: string): void;
16
+ /**
17
+ * Adds a class value with the rules of clsx (class on an element): a string, an array (nested too),
18
+ * or an object whose keys are added when their value is truthy. Joining an array would give "a,b"
19
+ */
20
+ export declare function addClassValue(classes: Set<string>, value: unknown): void;
@@ -0,0 +1,64 @@
1
+ const endsWithSeparator = /[^a-zA-Z0-9]$/;
2
+ const startsWithSeparator = /^[^a-zA-Z0-9]/;
3
+ const whitespace = /\s/;
4
+ const whitespaces = /\s+/;
5
+ /**
6
+ * The classes of the s-class_xxx directives (class:xxx on a kit tag):
7
+ * - true gives the name (class:active -> active);
8
+ * - a number or a text gives the name and the value, joined by a dash (class:gap={4} -> gap-4),
9
+ * without a dash when the name ends or the value starts with a separator (- _ : / .):
10
+ * s-class_gap-={4}, s-class_variant="-primary" and s-class_state=":active" keep working;
11
+ * - false, null, undefined and '' give nothing.
12
+ */
13
+ export function directiveClasses(classProps) {
14
+ const classes = [];
15
+ if (!classProps)
16
+ return classes;
17
+ for (const key in classProps) {
18
+ const value = classProps[key];
19
+ // 8 = 's-class_'.length
20
+ const base = key.slice(8);
21
+ if (value === true)
22
+ classes.push(base);
23
+ else if ((typeof value === 'string' && value) || typeof value === 'number') {
24
+ const text = String(value);
25
+ const dash = endsWithSeparator.test(base) || startsWithSeparator.test(text) ? '' : '-';
26
+ classes.push(`${base}${dash}${text}`);
27
+ }
28
+ }
29
+ return classes;
30
+ }
31
+ /**
32
+ * Adds the classes of a value (one or several, separated by spaces) to the set.
33
+ * The set keeps the first position of each class and drops the duplicates.
34
+ */
35
+ export function addClasses(classes, value) {
36
+ // Most values are a single class: no split (the costly part)
37
+ if (!whitespace.test(value)) {
38
+ if (value)
39
+ classes.add(value);
40
+ return;
41
+ }
42
+ for (const name of value.split(whitespaces)) {
43
+ if (name)
44
+ classes.add(name);
45
+ }
46
+ }
47
+ /**
48
+ * Adds a class value with the rules of clsx (class on an element): a string, an array (nested too),
49
+ * or an object whose keys are added when their value is truthy. Joining an array would give "a,b"
50
+ */
51
+ export function addClassValue(classes, value) {
52
+ if (typeof value === 'string')
53
+ addClasses(classes, value);
54
+ else if (Array.isArray(value)) {
55
+ for (const item of value)
56
+ addClassValue(classes, item);
57
+ }
58
+ else if (value && typeof value === 'object') {
59
+ for (const key in value) {
60
+ if (value[key])
61
+ addClasses(classes, key);
62
+ }
63
+ }
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lapikit",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,8 @@
35
35
  "README.md",
36
36
  "LICENSE",
37
37
  "!dist/**/*.test.*",
38
- "!dist/**/*.spec.*"
38
+ "!dist/**/*.spec.*",
39
+ "!dist/**/*.scss"
39
40
  ],
40
41
  "sideEffects": [
41
42
  "**/*.css"
@@ -104,6 +105,7 @@
104
105
  "prettier": "^3.4.2",
105
106
  "prettier-plugin-svelte": "^3.3.3",
106
107
  "publint": "^0.3.2",
108
+ "sass": "^1.105.1",
107
109
  "svelte": "^5.0.0",
108
110
  "svelte-check": "^4.0.0",
109
111
  "typescript": "^5.0.0",