@thinkpixellab-public/px-vue 4.1.22 → 4.1.24

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.
@@ -11,6 +11,7 @@
11
11
  class="px-qr-code"
12
12
  ref="qrCode"
13
13
  :style="{ width: size + 'px', height: size + 'px' }"
14
+ role="img"
14
15
  :aria-label="label || content"
15
16
  ></div>
16
17
  </div>
@@ -0,0 +1,264 @@
1
+ /**
2
+ * useCornerSmoothing()
3
+ *
4
+ * A Vue 3 composable for creating smooth, squircle-like corners using CSS clip-path.
5
+ * Based on Figma's corner smoothing approach.
6
+ *
7
+ * Usage:
8
+ * ```vue
9
+ * <script>
10
+ * import { ref } from 'vue'
11
+ * import { useCornerSmoothing } from '@thinkpixellab-public/px-vue/composables/useCornerSmoothing'
12
+ *
13
+ * export default {
14
+ * setup() {
15
+ * const myElement = ref(null)
16
+ * const { clipPath } = useCornerSmoothing(myElement, {
17
+ * radius: 16,
18
+ * smoothing: 0.8
19
+ * })
20
+ *
21
+ * return {
22
+ * myElement,
23
+ * clipPath // already reactive (computed)
24
+ * }
25
+ * }
26
+ * }
27
+ * </script>
28
+ *
29
+ * <template>
30
+ * <div
31
+ * ref="myElement"
32
+ * :style="{ clipPath }"
33
+ * >
34
+ * Content with smooth corners
35
+ * </div>
36
+ * </template>
37
+ * ```
38
+ *
39
+ * @param {element} elementRef Vue ref to the target element
40
+ * @param {number|Array|string} options.radius number, array [tl,tr,br,bl], or percentage string
41
+ * @param {number} options.smoothing smoothing amount 0-1 (default: 0.8)
42
+ * @param {number} options.borderWidth border width to account for (default: 0)
43
+ * @param {number} options.minRadius min radius in px (to constrain percentages)
44
+ * @param {number} options.maxRadius max radius in px (to constrain percentages)
45
+ * @param {boolean} options.autoApply Auto-apply clip-path to element style
46
+ * @param {boolean} options.preserveSmoothing Maintain smoothing at larger radii (See figma-squircle docs)
47
+ */
48
+
49
+ import { computed, onMounted, onUnmounted, nextTick, reactive, watch } from 'vue';
50
+ import { getSvgPath } from 'figma-squircle';
51
+
52
+ function useCornerSmoothing(elementRef, options = {}) {
53
+ const dimensions = reactive({ width: 0, height: 0 });
54
+
55
+ let resizeObserver = null;
56
+
57
+ const config = reactive({
58
+ radius: 8,
59
+ smoothing: 0.8,
60
+ borderWidth: 0,
61
+ minRadius: 0,
62
+ maxRadius: Infinity,
63
+ autoApply: false,
64
+ preserveSmoothing: true,
65
+ ...options,
66
+ });
67
+
68
+ /**
69
+ * parse percentage string to number
70
+ */
71
+ function parsePercentage(value, reference) {
72
+ if (typeof value === 'string' && value.endsWith('%')) {
73
+ const percent = parseFloat(value) / 100;
74
+ return reference * percent;
75
+ }
76
+ return parseFloat(value) || 0;
77
+ }
78
+
79
+ /**
80
+ * expand radius array using css border-radius rules
81
+ */
82
+ function expandRadiusArray(radius) {
83
+ if (!Array.isArray(radius)) {
84
+ return [radius, radius, radius, radius];
85
+ }
86
+
87
+ const [tl, tr = tl, br = tl, bl = tr] = radius;
88
+ return [tl, tr, br, bl];
89
+ }
90
+
91
+ /**
92
+ * calculate corner radii with percentage support and clamping
93
+ */
94
+ function calculateCornerRadii(radius, width, height, minRadius, maxRadius) {
95
+ const reference = Math.max(width, height);
96
+ const expanded = expandRadiusArray(radius);
97
+
98
+ return expanded.map(r => {
99
+ const parsed = parsePercentage(r, reference);
100
+ return Math.max(minRadius, Math.min(maxRadius, parsed));
101
+ });
102
+ }
103
+
104
+ /**
105
+ * generates squircle path using figma-squircle library
106
+ */
107
+ function generateSquirclePath(width, height, radii, smoothing, preserveSmoothing) {
108
+ if (width <= 0 || height <= 0) return '';
109
+
110
+ const [tl, tr, br, bl] = radii;
111
+
112
+ // check if all corners are the same (uniform)
113
+ const isUniform = tl === tr && tr === br && br === bl;
114
+
115
+ if (isUniform) {
116
+ // use simple API for uniform corners
117
+ return getSvgPath({
118
+ width,
119
+ height,
120
+ cornerRadius: tl,
121
+ cornerSmoothing: smoothing,
122
+ preserveSmoothing,
123
+ });
124
+ } else {
125
+ // use individual corner API for different corners
126
+ return getSvgPath({
127
+ width,
128
+ height,
129
+ cornerRadius: Math.max(tl, tr, br, bl), // fallback base radius
130
+ cornerSmoothing: smoothing,
131
+ preserveSmoothing,
132
+ topLeftCornerRadius: tl,
133
+ topRightCornerRadius: tr,
134
+ bottomRightCornerRadius: br,
135
+ bottomLeftCornerRadius: bl,
136
+ });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Computed clip-path value
142
+ */
143
+ const clipPath = computed(() => {
144
+ if (dimensions.width <= 0 || dimensions.height <= 0) return null;
145
+
146
+ const adjustedWidth = Math.max(0, dimensions.width - config.borderWidth * 2);
147
+ const adjustedHeight = Math.max(0, dimensions.height - config.borderWidth * 2);
148
+
149
+ if (adjustedWidth <= 0 || adjustedHeight <= 0) return null;
150
+
151
+ // calculate corner radii with percentage support and clamping
152
+ const radii = calculateCornerRadii(
153
+ config.radius,
154
+ adjustedWidth,
155
+ adjustedHeight,
156
+ config.minRadius,
157
+ config.maxRadius,
158
+ );
159
+
160
+ const path = generateSquirclePath(
161
+ adjustedWidth,
162
+ adjustedHeight,
163
+ radii,
164
+ config.smoothing,
165
+ config.preserveSmoothing,
166
+ );
167
+
168
+ if (!path) return null;
169
+
170
+ // offset path if border width is specified
171
+ const offsetX = config.borderWidth;
172
+ const offsetY = config.borderWidth;
173
+
174
+ if (offsetX > 0 || offsetY > 0) {
175
+ // transform path to account for border offset
176
+ const translatedPath = path
177
+ .replace(/([ML])\s*([0-9.]+)\s+([0-9.]+)/g, (_, command, x, y) => {
178
+ return `${command} ${parseFloat(x) + offsetX} ${parseFloat(y) + offsetY}`;
179
+ })
180
+ .replace(
181
+ /([C])\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)/g,
182
+ (_, command, x1, y1, x2, y2, x3, y3) => {
183
+ return `${command} ${parseFloat(x1) + offsetX} ${parseFloat(y1) + offsetY} ${parseFloat(x2) + offsetX} ${parseFloat(y2) + offsetY} ${parseFloat(x3) + offsetX} ${parseFloat(y3) + offsetY}`;
184
+ },
185
+ );
186
+ return `path('${translatedPath}')`;
187
+ }
188
+
189
+ return `path('${path}')`;
190
+ });
191
+
192
+ /**
193
+ * Update element dimensions
194
+ */
195
+ function updateDimensions() {
196
+ if (!elementRef.value) return;
197
+
198
+ const rect = elementRef.value.getBoundingClientRect();
199
+ dimensions.width = rect.width;
200
+ dimensions.height = rect.height;
201
+ }
202
+
203
+ /**
204
+ * Apply clip-path directly to element style
205
+ */
206
+ function applyClipPath() {
207
+ if (!elementRef.value || !config.autoApply) return;
208
+
209
+ if (clipPath.value) {
210
+ elementRef.value.style.clipPath = clipPath.value;
211
+ } else {
212
+ elementRef.value.style.clipPath = '';
213
+ }
214
+ }
215
+
216
+ // watch for auto-apply changes
217
+ watch([clipPath, () => config.autoApply], applyClipPath, { immediate: true });
218
+
219
+ onMounted(() => {
220
+ nextTick(() => {
221
+ if (!elementRef.value) return;
222
+
223
+ // initial dimension update
224
+ updateDimensions();
225
+
226
+ // set up resize observer
227
+ resizeObserver = new ResizeObserver(entries => {
228
+ for (const entry of entries) {
229
+ dimensions.width = entry.contentRect.width;
230
+ dimensions.height = entry.contentRect.height;
231
+ }
232
+ });
233
+
234
+ resizeObserver.observe(elementRef.value);
235
+ });
236
+ });
237
+
238
+ onUnmounted(() => {
239
+ resizeObserver?.disconnect();
240
+ });
241
+
242
+ /**
243
+ * Update configuration reactively
244
+ */
245
+ function updateConfig(newConfig) {
246
+ Object.assign(config, newConfig);
247
+ }
248
+
249
+ /**
250
+ * Manual update trigger
251
+ */
252
+ function forceUpdate() {
253
+ updateDimensions();
254
+ }
255
+
256
+ return {
257
+ clipPath,
258
+ updateConfig,
259
+ forceUpdate,
260
+ };
261
+ }
262
+
263
+ export { useCornerSmoothing };
264
+ export { useCornerSmoothing as default };
@@ -1,5 +1,57 @@
1
+ /**
2
+ * useVariants()
3
+ *
4
+ * Handles component variants as a replacement for PxBaseVariants. Provides reactive variant
5
+ * processing with support for defaults and base variants.
6
+ *
7
+ * Usage with Options API setup():
8
+ *
9
+ * ```vue
10
+ * <script>
11
+ * import useVariants from '@thinkpixellab-public/px-vue/composables/useVariants'
12
+ * import { inject, computed } from 'vue'
13
+ *
14
+ * export default {
15
+ * props: {
16
+ * variant: { type: String, default: null },
17
+ * },
18
+ * setup(props) {
19
+ * const fallback = inject('defaultVariant', null)
20
+ *
21
+ * const { variantsMap, isVariant, variantsToValue } = useVariants(
22
+ * props.variant, // main variant prop
23
+ * fallback, // default/fallback variant
24
+ * 'base-class' // base variant (optional)
25
+ * )
26
+ *
27
+ * const isSpecial = computed(() => isVariant('special'))
28
+ * const buttonColor = variantsToValue({ primary: 'blue', secondary: 'gray' }, 'default')
29
+ *
30
+ * return {
31
+ * variantsMap, // for :class="bem(variantsMap)"
32
+ * isSpecial,
33
+ * buttonColor,
34
+ * }
35
+ * }
36
+ * }
37
+ * </script>
38
+ *
39
+ * <template>
40
+ * <div :class="bem(variantsMap)">
41
+ * Content with variants applied
42
+ * </div>
43
+ * </template>
44
+ * ```
45
+ *
46
+ * @param {string|Array|Ref} variant Main variant value (reactive)
47
+ * @param {string|Array|Ref} variantDefault Default variant when main is empty (reactive)
48
+ * @param {string|Array|Ref} variantBase Base variant always included (reactive)
49
+ * @returns {Object} { variants, variantArray, variantString, variantsMap, isVariant,
50
+ * variantsToValue }
51
+ */
52
+
1
53
  import { computed, toValue } from 'vue';
2
- import variants from '../utils/variants.js';
54
+ import variantsUtils from '../utils/variants.js';
3
55
  import { arrayHasCommonItem, ensureArray } from '../utils/utils.js';
4
56
 
5
57
  export default function useVariants(variant, variantDefault, variantBase) {
@@ -11,12 +63,12 @@ export default function useVariants(variant, variantDefault, variantBase) {
11
63
  * Returns the variant as an array. This is also the primary source of state for the variant and
12
64
  * should be used by all other methods, etc.
13
65
  */
14
- const variants = computed(() => {
15
- const variantArray = variants.toVariantsArray(toValue(variant));
16
- const baseArray = variants.toVariantsArray(toValue(variantBase));
17
- const defaultArray = variants.toVariantsArray(toValue(variantDefault));
66
+ const variantArray = computed(() => {
67
+ const variantArray = variantsUtils.toVariantsArray(toValue(variant));
68
+ const baseArray = variantsUtils.toVariantsArray(toValue(variantBase));
69
+ const defaultArray = variantsUtils.toVariantsArray(toValue(variantDefault));
18
70
 
19
- return this.toVariantsArray([
71
+ return variantsUtils.toVariantsArray([
20
72
  ...baseArray,
21
73
  ...(variantArray.length ? variantArray : defaultArray),
22
74
  ]);
@@ -26,7 +78,7 @@ export default function useVariants(variant, variantDefault, variantBase) {
26
78
  * Returns the variant as a string
27
79
  */
28
80
  const variantString = computed(() => {
29
- return this.variants.join(' ');
81
+ return variantArray.value.join(' ');
30
82
  });
31
83
 
32
84
  /**
@@ -34,9 +86,12 @@ export default function useVariants(variant, variantDefault, variantBase) {
34
86
  * methods, etc.)
35
87
  */
36
88
  const variantsMap = computed(() => {
37
- return this.toVariantsMap(this.variants);
89
+ return variantsUtils.toVariantsMap(variantArray.value);
38
90
  });
39
91
 
92
+ // alias for backwards compatibility with PxBaseVariants
93
+ const variants = variantArray;
94
+
40
95
  // -------------------------------------------------------------------------------
41
96
  // Methods
42
97
  // -------------------------------------------------------------------------------
@@ -46,7 +101,7 @@ export default function useVariants(variant, variantDefault, variantBase) {
46
101
  */
47
102
  const isVariant = variantStringOrArray => {
48
103
  const array = ensureArray(variantStringOrArray);
49
- return arrayHasCommonItem(this.variants, array);
104
+ return arrayHasCommonItem(variantArray.value, array);
50
105
  };
51
106
 
52
107
  /**
@@ -56,12 +111,12 @@ export default function useVariants(variant, variantDefault, variantBase) {
56
111
  */
57
112
  const variantsToValue = (valueMap, fallback) => {
58
113
  for (let key in valueMap) {
59
- if (this.isVariant(key)) {
114
+ if (isVariant(key)) {
60
115
  return valueMap[key];
61
116
  }
62
117
  }
63
118
  return fallback;
64
119
  };
65
120
 
66
- return { variants, variantString, variantsMap, isVariant, variantsToValue };
121
+ return { variants, variantArray, variantString, variantsMap, isVariant, variantsToValue };
67
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "4.1.22",
3
+ "version": "4.1.24",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": {
6
6
  "name": "Pixel Lab"
@@ -28,6 +28,7 @@
28
28
  "body-scroll-lock": "^3.1.5",
29
29
  "chrono-node": "^2.7.8",
30
30
  "dateformat": "^5.0.3",
31
+ "figma-squircle": "^1.1.0",
31
32
  "gsap": "^3.12.7",
32
33
  "highlight.js": "^11.11.1",
33
34
  "mitt": "^3.0.1",