@thinkpixellab-public/px-vue 4.1.23 → 4.1.26

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.
@@ -139,15 +139,19 @@ export default {
139
139
  immediate: true,
140
140
  handler(nv, ov) {
141
141
  // check for matching values
142
- if (nv && ov && nv.length == ov.length) {
142
+ const nvArray = ensureSet(nv);
143
+ const ovArray = ensureSet(ov);
144
+
145
+ if (nvArray.length === ovArray.length) {
143
146
  let match = true;
144
- for (let i = 0; i < nv.length; i++) {
145
- match = match && nv[i] == ov[i];
147
+ for (let i = 0; i < nvArray.length; i++) {
148
+ match = match && nvArray[i] === ovArray[i];
146
149
  }
147
150
  if (match) {
148
151
  return;
149
152
  }
150
153
  }
154
+
151
155
  this.setSelection(nv);
152
156
  },
153
157
  },
@@ -21,7 +21,7 @@ export default {
21
21
  .px-card {
22
22
  /// Configuration defautls for px-calendar
23
23
  $-defaults: (
24
- 'px-card.background-color': color(page-bg),
24
+ 'px-card.background-color': site-var(page-bg),
25
25
  'px-card.padding': 1rem,
26
26
  'px-card.border-radius': 0.5rem,
27
27
  'px-card.box-shadow': depth-shadow(20, 0.1, black),
@@ -0,0 +1,233 @@
1
+ <!--
2
+ Describe this component...
3
+ -->
4
+ <template>
5
+ <div
6
+ v-if="smoothingEnabled"
7
+ :class="bem()"
8
+ ref="el"
9
+ :style="{
10
+ position: 'relative',
11
+ clipPath: renderMode === 'clip' ? smoothCornersPath : null,
12
+ mask: renderMode === 'mask' ? smoothCornersPath : null,
13
+ ...overrideStyles,
14
+ }"
15
+ >
16
+ <div
17
+ v-if="hasBorder"
18
+ :style="{
19
+ position: 'absolute',
20
+ inset: 0,
21
+ clipPath: renderMode === 'clip' ? borderMaskPath : null,
22
+ mask: renderMode === 'mask' ? borderMaskPath : null,
23
+ backgroundColor: computedBorderColor,
24
+ touchAction: 'none',
25
+ }"
26
+ ></div>
27
+
28
+ <slot />
29
+ </div>
30
+
31
+ <div v-else>
32
+ <slot />
33
+ </div>
34
+ </template>
35
+
36
+ <script>
37
+ import { getSmoothCornersPath, getBorderMaskClipPath } from '../utils/smootherCorners';
38
+
39
+ export default {
40
+ name: 'px-corners',
41
+ props: {
42
+ smoothingEnabled: { type: Boolean, default: true },
43
+ smoothing: { type: Number, default: 1 },
44
+ maxRadius: { type: [Number, String], default: Infinity },
45
+ minRadius: { type: [Number, String], default: 0 },
46
+ radius: { type: [Array, Number, String], default: 'auto' },
47
+ borderWidth: { type: [Number, String], default: 'auto' },
48
+ borderColor: { type: String, default: 'auto' },
49
+
50
+ // clip or mask
51
+ renderMode: { type: String, default: 'clip' },
52
+ },
53
+ data() {
54
+ return {
55
+ dimensions: {
56
+ width: 0,
57
+ height: 0,
58
+ },
59
+ computedRadius: null,
60
+ computedBorderWidth: 0,
61
+ computedBorderColor: null,
62
+ resizeObserver: null,
63
+
64
+ // flag that we've detected auto styles
65
+ autoStylesDetected: false,
66
+ };
67
+ },
68
+ computed: {
69
+ smoothCornersPath() {
70
+ if (
71
+ !this.smoothingEnabled ||
72
+ this.dimensions.width <= 0 ||
73
+ this.dimensions.height <= 0
74
+ ) {
75
+ return null;
76
+ }
77
+
78
+ return getSmoothCornersPath({
79
+ width: this.dimensions.width,
80
+ height: this.dimensions.height,
81
+ radius: this.computedRadius || this.radius,
82
+ smoothing: this.smoothing,
83
+ borderWidth: 0,
84
+ minRadius: this.minRadius,
85
+ maxRadius: this.maxRadius,
86
+ preserveSmoothing: true,
87
+ renderMode: this.renderMode,
88
+ });
89
+ },
90
+ borderMaskPath() {
91
+ if (
92
+ !this.smoothingEnabled ||
93
+ this.dimensions.width <= 0 ||
94
+ this.dimensions.height <= 0 ||
95
+ this.computedBorderWidth <= 0
96
+ ) {
97
+ return null;
98
+ }
99
+
100
+ return getBorderMaskClipPath({
101
+ width: this.dimensions.width,
102
+ height: this.dimensions.height,
103
+ radius: this.computedRadius || this.radius,
104
+ smoothing: this.smoothing,
105
+ borderWidth: this.computedBorderWidth,
106
+ minRadius: this.minRadius,
107
+ maxRadius: this.maxRadius,
108
+ preserveSmoothing: true,
109
+ renderMode: this.renderMode,
110
+ });
111
+ },
112
+ hasBorder() {
113
+ return !!this.borderMaskPath;
114
+ },
115
+ overrideStyles() {
116
+ if (!this.smoothingEnabled || !this.autoStylesDetected) return {};
117
+
118
+ return {
119
+ borderRadius: 'unset',
120
+ borderTopLeftRadius: 'unset',
121
+ borderTopRightRadius: 'unset',
122
+ borderBottomRightRadius: 'unset',
123
+ borderBottomLeftRadius: 'unset',
124
+ border: 'unset',
125
+ borderWidth: 'unset',
126
+ borderStyle: 'unset',
127
+ borderColor: 'unset',
128
+ };
129
+ },
130
+ },
131
+ mounted() {
132
+ this.$nextTick(() => {
133
+ if (!this.$refs.el) return;
134
+ if (!this.smoothingEnabled) return;
135
+
136
+ this.detectAutoValues();
137
+ this.updateDimensions();
138
+ this.setupResizeObserver();
139
+ });
140
+ },
141
+ beforeUnmount() {
142
+ if (this.resizeObserver) {
143
+ this.resizeObserver.disconnect();
144
+ }
145
+ },
146
+ methods: {
147
+ parseCssDimension(value) {
148
+ if (typeof value === 'string' && value.length > 0) {
149
+ value = value.trim().split(/\s+/)[0];
150
+
151
+ if (value.endsWith('px')) return parseFloat(value);
152
+ if (value.endsWith('rem')) {
153
+ return (
154
+ parseFloat(value) *
155
+ parseFloat(getComputedStyle(document.documentElement).fontSize)
156
+ );
157
+ }
158
+ if (value.endsWith('em')) {
159
+ return parseFloat(value) * parseFloat(getComputedStyle(this.$refs.el).fontSize);
160
+ }
161
+ }
162
+
163
+ return value;
164
+ },
165
+ detectAutoValues() {
166
+ if (!this.$refs.el) return;
167
+
168
+ const cs = getComputedStyle(this.$refs.el);
169
+
170
+ // auto radius detection
171
+ if (this.radius === 'auto') {
172
+ const radiusTopLeft = cs.getPropertyValue('border-top-left-radius') || '0';
173
+ const radiusTopRight = cs.getPropertyValue('border-top-right-radius') || '0';
174
+ const radiusBottomRight = cs.getPropertyValue('border-bottom-right-radius') || '0';
175
+ const radiusBottomLeft = cs.getPropertyValue('border-bottom-left-radius') || '0';
176
+
177
+ this.computedRadius = [
178
+ radiusTopLeft,
179
+ radiusTopRight,
180
+ radiusBottomRight,
181
+ radiusBottomLeft,
182
+ ].map(r => this.parseCssDimension(r));
183
+ }
184
+
185
+ // auto border width detection
186
+ if (this.borderWidth === 'auto') {
187
+ // try border-width first, fallback to border-top-width
188
+ let borderWidth = cs.getPropertyValue('border-width') || '0';
189
+ if (borderWidth === '0' || borderWidth === '0px') {
190
+ borderWidth = cs.getPropertyValue('border-top-width') || '0';
191
+ }
192
+ this.computedBorderWidth = this.parseCssDimension(borderWidth);
193
+ } else {
194
+ this.computedBorderWidth = parseFloat(this.borderWidth) || 0;
195
+ }
196
+
197
+ // auto border color detection
198
+ if (this.borderColor === 'auto') {
199
+ this.computedBorderColor = cs.getPropertyValue('border-color') || 'transparent';
200
+ } else {
201
+ this.computedBorderColor = this.borderColor;
202
+ }
203
+
204
+ // flag that we've detected styles, now we can apply overrides
205
+ this.autoStylesDetected = true;
206
+ },
207
+ updateDimensions() {
208
+ if (!this.$refs.el) return;
209
+
210
+ const rect = this.$refs.el.getBoundingClientRect();
211
+ this.dimensions.width = rect.width;
212
+ this.dimensions.height = rect.height;
213
+ },
214
+ setupResizeObserver() {
215
+ if (!this.$refs.el) return;
216
+
217
+ this.resizeObserver = new ResizeObserver(entries => {
218
+ for (const entry of entries) {
219
+ this.dimensions.width = entry.contentRect.width;
220
+ this.dimensions.height = entry.contentRect.height;
221
+ }
222
+ });
223
+
224
+ this.resizeObserver.observe(this.$refs.el);
225
+ },
226
+ },
227
+ };
228
+ </script>
229
+
230
+ <!-- <style lang="scss" scoped>
231
+ // @use '@/styles/include.scss' as *;
232
+ @use '../styles/px.scss' as *;
233
+ </style> -->
@@ -190,7 +190,7 @@ export default {
190
190
  $popup: get(
191
191
  'controls.popup',
192
192
  (
193
- background-color: color(page-bg),
193
+ background-color: site-var(page-bg),
194
194
  border-radius: get('controls.base.border-radius'),
195
195
  box-shadow: shadow(15),
196
196
  font-size: get('controls.base.font-size'),
@@ -198,8 +198,8 @@ export {
198
198
 
199
199
  .px-menu {
200
200
  font-family: get('font-family');
201
- background-color: color(page-bg);
202
- color: color(page-fg);
201
+ background-color: site-var(page-bg);
202
+ color: site-var(page-fg);
203
203
  border-radius: get('controls.border-radius');
204
204
  overflow: hidden;
205
205
  display: flex;
@@ -121,15 +121,15 @@ function mouseMove(e) {
121
121
  <style lang="scss">
122
122
  @use '../styles/px.scss' as *;
123
123
 
124
- .px-scrubber {
125
- @at-root :root {
126
- --px-scrubber-height: 4px;
127
- --px-scrubber-bg: #{clr(accent, $alpha: 0.1)};
128
- --px-scrubber-padding: 1em 0;
129
- --px-scrubber-progress-bg: #{clr(accent, 1)};
130
- --px-scrubber-preview-bg: #{clr(accent, $alpha: 0.33)};
131
- }
124
+ @at-root :root {
125
+ --px-scrubber-height: 4px;
126
+ --px-scrubber-bg: #{clr(accent, $alpha: 0.1)};
127
+ --px-scrubber-padding: 1em 0;
128
+ --px-scrubber-progress-bg: #{clr(accent, 1)};
129
+ --px-scrubber-preview-bg: #{clr(accent, $alpha: 0.33)};
130
+ }
132
131
 
132
+ .px-scrubber {
133
133
  width: 100%;
134
134
  padding: var(--px-scrubber-padding);
135
135
  cursor: pointer;
@@ -0,0 +1,253 @@
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 { getSmoothCornersPath, getBorderMaskClipPath } from '../utils/smootherCorners';
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: 'auto',
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
+ * Computed clip-path value for the outer container (full size, ignoring borderWidth)
70
+ */
71
+ const clipPath = computed(() => {
72
+ if (dimensions.width <= 0 || dimensions.height <= 0) return null;
73
+
74
+ return getSmoothCornersPath({
75
+ width: dimensions.width,
76
+ height: dimensions.height,
77
+ radius: config.radius,
78
+ smoothing: config.smoothing,
79
+ borderWidth: 0, // always 0 for outer container
80
+ minRadius: config.minRadius,
81
+ maxRadius: config.maxRadius,
82
+ preserveSmoothing: config.preserveSmoothing,
83
+ });
84
+ });
85
+
86
+ /**
87
+ * Computed clip-path value for inner content (accounting for borderWidth to create border space)
88
+ */
89
+ const innerClipPath = computed(() => {
90
+ if (dimensions.width <= 0 || dimensions.height <= 0) return null;
91
+ if (config.borderWidth <= 0) return clipPath.value; // no border, use full path
92
+
93
+ return getSmoothCornersPath({
94
+ width: dimensions.width,
95
+ height: dimensions.height,
96
+ radius: config.radius,
97
+ smoothing: config.smoothing,
98
+ borderWidth: config.borderWidth, // reduce for border space
99
+ minRadius: config.minRadius,
100
+ maxRadius: config.maxRadius,
101
+ preserveSmoothing: config.preserveSmoothing,
102
+ });
103
+ });
104
+
105
+ /**
106
+ * Computed border mask clip-path (full rectangle with smooth hole cut out)
107
+ */
108
+ const borderMaskClipPath = computed(() => {
109
+ if (dimensions.width <= 0 || dimensions.height <= 0) return null;
110
+ if (config.borderWidth <= 0) return null;
111
+
112
+ return getBorderMaskClipPath({
113
+ width: dimensions.width,
114
+ height: dimensions.height,
115
+ radius: config.radius,
116
+ smoothing: config.smoothing,
117
+ borderWidth: config.borderWidth,
118
+ minRadius: config.minRadius,
119
+ maxRadius: config.maxRadius,
120
+ preserveSmoothing: config.preserveSmoothing,
121
+ });
122
+ });
123
+
124
+ /**
125
+ * Update element dimensions
126
+ */
127
+ function updateDimensions() {
128
+ if (!elementRef.value) return;
129
+
130
+ const rect = elementRef.value.getBoundingClientRect();
131
+ dimensions.width = rect.width;
132
+ dimensions.height = rect.height;
133
+ }
134
+
135
+ /**
136
+ * Apply clip-path directly to element style
137
+ */
138
+ function applyClipPath() {
139
+ if (!elementRef.value || !config.autoApply) return;
140
+
141
+ if (clipPath.value) {
142
+ elementRef.value.style.clipPath = clipPath.value;
143
+ } else {
144
+ elementRef.value.style.clipPath = '';
145
+ }
146
+ }
147
+
148
+ // watch for auto-apply changes
149
+ watch([clipPath, () => config.autoApply], applyClipPath, { immediate: true });
150
+
151
+ onMounted(() => {
152
+ nextTick(() => {
153
+ if (!elementRef.value) return;
154
+
155
+ function parseCssDimension(value) {
156
+ if (typeof value === 'string' && value.length > 0) {
157
+ value = value.trim().split(/\s+/)[0];
158
+
159
+ if (value.endsWith('px')) return parseFloat(value);
160
+ if (value.endsWith('rem')) {
161
+ return (
162
+ parseFloat(value) *
163
+ parseFloat(getComputedStyle(document.documentElement).fontSize)
164
+ );
165
+ }
166
+ if (value.endsWith('em')) {
167
+ return (
168
+ parseFloat(value) *
169
+ parseFloat(getComputedStyle(elementRef.value).fontSize)
170
+ );
171
+ }
172
+ }
173
+
174
+ return value;
175
+ }
176
+
177
+ let cs = null;
178
+
179
+ // auto radius
180
+
181
+ if (config.radius === 'auto') {
182
+ // auto-detect radius based on current styles
183
+ cs = getComputedStyle(elementRef.value);
184
+ const radiusTopLeft = cs.getPropertyValue('border-top-left-radius') || '0';
185
+ const radiusTopRight = cs.getPropertyValue('border-top-right-radius') || '0';
186
+ const radiusBottomRight = cs.getPropertyValue('border-bottom-right-radius') || '0';
187
+ const radiusBottomLeft = cs.getPropertyValue('border-bottom-left-radius') || '0';
188
+ config.radius = [
189
+ radiusTopLeft,
190
+ radiusTopRight,
191
+ radiusBottomRight,
192
+ radiusBottomLeft,
193
+ ].map(r => {
194
+ return parseCssDimension(r, true);
195
+ });
196
+ }
197
+
198
+ // auto border width
199
+ if (config.borderWidth === 'auto') {
200
+ cs = cs || getComputedStyle(elementRef.value);
201
+ const borderWidth = cs.getPropertyValue('border-thickness') || '0';
202
+
203
+ console.log('bw' + cs.getPropertyValue('border-thickness'));
204
+
205
+ config.borderWidth = parseCssDimension(borderWidth, true);
206
+ }
207
+
208
+ console.log(config.borderWidth);
209
+
210
+ // initial dimension update
211
+ updateDimensions();
212
+
213
+ // set up resize observer
214
+ resizeObserver = new ResizeObserver(entries => {
215
+ for (const entry of entries) {
216
+ dimensions.width = entry.contentRect.width;
217
+ dimensions.height = entry.contentRect.height;
218
+ }
219
+ });
220
+
221
+ resizeObserver.observe(elementRef.value);
222
+ });
223
+ });
224
+
225
+ onUnmounted(() => {
226
+ resizeObserver?.disconnect();
227
+ });
228
+
229
+ /**
230
+ * Update configuration reactively
231
+ */
232
+ function updateConfig(newConfig) {
233
+ Object.assign(config, newConfig);
234
+ }
235
+
236
+ /**
237
+ * Manual update trigger
238
+ */
239
+ function forceUpdate() {
240
+ updateDimensions();
241
+ }
242
+
243
+ return {
244
+ clipPath,
245
+ innerClipPath,
246
+ borderMaskClipPath,
247
+ updateConfig,
248
+ forceUpdate,
249
+ };
250
+ }
251
+
252
+ export { useCornerSmoothing };
253
+ 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.23",
3
+ "version": "4.1.26",
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",
@@ -0,0 +1,187 @@
1
+ // Reference:
2
+ // https://storybook.js.org/blog/storybook-7-docs/
3
+ // https://storybook.js.org/docs/7.5/writing-docs/autodocs
4
+
5
+ import PxCorners from '@/components/PxCorners.vue';
6
+ import { extractArgTypes } from '@/storybook/sb-utils.js';
7
+
8
+ export default {
9
+ component: PxCorners,
10
+ // decorators: [ ... ],
11
+ // parameters: { ... },
12
+
13
+ // https://storybook.js.org/docs/7.0/vue/essentials/controls#annotation
14
+ // https://storybook.js.org/docs/7.0/vue/api/argtypes#available-properties
15
+ argTypes: {
16
+ ...extractArgTypes(PxCorners),
17
+ // someProp: { type: 'select', options: ['one', 'two', 'three'] }
18
+ },
19
+ args: {
20
+ // label: 'Hello',
21
+ // onClick: () => {}),
22
+ },
23
+ };
24
+
25
+ export const Compare = {
26
+ // name: 'Story Name',
27
+ // parameters: {
28
+ // customCSS: `.myclass { border: 4px solid red; }`,
29
+ // },
30
+ render: (args, { argTypes }) => ({
31
+ components: { PxCorners },
32
+ setup() {
33
+ return { args };
34
+ },
35
+ template: `
36
+ <div class="story-flex" style=" position: relative;">
37
+ <div v-bind="args" class="story-bg-blue" style="border-radius: 50px; width: 200px; height: 200px;" />
38
+ <PxCorners v-bind="args" class="story-bg-orange" :radius="50" style="width: 200px; height: 200px;" />
39
+ </div>
40
+ `,
41
+ }),
42
+ };
43
+
44
+ export const Basic = {
45
+ // name: 'Story Name',
46
+ // parameters: {
47
+ // customCSS: `.myclass { border: 4px solid red; }`,
48
+ // },
49
+ render: (args, { argTypes }) => ({
50
+ components: { PxCorners },
51
+ setup() {
52
+ return { args };
53
+ },
54
+ template: `<PxCorners v-bind="args" class="story-size-md story-bg-orange" :radius="40" :borderWidth="4" borderColor="black" />`,
55
+ }),
56
+ };
57
+
58
+ export const Slot = {
59
+ // name: 'Story Name',
60
+ // parameters: {
61
+ // customCSS: `.myclass { border: 4px solid red; }`,
62
+ // },
63
+ render: (args, { argTypes }) => ({
64
+ components: { PxCorners },
65
+ setup() {
66
+ return { args };
67
+ },
68
+ template: `<PxCorners v-bind="args" class="story-size-md story-bg-blue story-center" :radius="40" > Slot content </PxCorners>`,
69
+ }),
70
+ };
71
+
72
+ export const SlotWithBorder = {
73
+ // name: 'Story Name',
74
+ // parameters: {
75
+ // customCSS: `.myclass { border: 4px solid red; }`,
76
+ // },
77
+ render: (args, { argTypes }) => ({
78
+ components: { PxCorners },
79
+ setup() {
80
+ return { args };
81
+ },
82
+ template: `<PxCorners v-bind="args" class="story-size-md story-bg-blue story-center" :radius="40" :borderWidth="4"> Slot content </PxCorners>`,
83
+ }),
84
+ };
85
+
86
+ export const HoverWithBorder = {
87
+ // name: 'Story Name',
88
+ // parameters: {
89
+ // customCSS: `.myclass { border: 4px solid red; }`,
90
+ // },
91
+ render: (args, { argTypes }) => ({
92
+ components: { PxCorners },
93
+ setup() {
94
+ return { args };
95
+ },
96
+ template: `<PxCorners v-bind="args" class="story-size-md story-bg-orange story-hover-bg-blue story-center" :radius="40" :borderWidth="10"> Slot content </PxCorners>`,
97
+ }),
98
+ };
99
+
100
+ export const AutoFromStyle = {
101
+ // name: 'Story Name',
102
+ // parameters: {
103
+ // customCSS: `.myclass { border: 4px solid red; }`,
104
+ // },
105
+ render: (args, { argTypes }) => ({
106
+ components: { PxCorners },
107
+ setup() {
108
+ return { args };
109
+ },
110
+ template: `<PxCorners :smoothingEnabled="true" v-bind="args" class="story-size-md story-bg-orange" style="border-radius: 40px; border: 4px solid black;" />`,
111
+ }),
112
+ };
113
+
114
+ export const AutoPercent = {
115
+ // name: 'Story Name',
116
+ // parameters: {
117
+ // customCSS: `.myclass { border: 4px solid red; }`,
118
+ // },
119
+ render: (args, { argTypes }) => ({
120
+ components: { PxCorners },
121
+ setup() {
122
+ return { args };
123
+ },
124
+ template: `
125
+ <div class="story-flex" style=" position: relative;">
126
+ <PxCorners :smoothingEnabled="true" v-bind="args" class="story-bg-orange story-center" style="width: 300px; height: 200px; border-radius: 20%;">
127
+ Style 20%
128
+ </PxCorners>
129
+ <PxCorners :smoothingEnabled="true" v-bind="args" class="story-bg-orange story-center" style="width: 300px; height: 200px;" :radius="60">
130
+ :radius 60px
131
+ </PxCorners>
132
+ <div class="story-bg-orange story-center" style="width: 300px; height: 200px; border-radius: 20%;">
133
+ Div 20%
134
+ </div>
135
+ <div class="story-bg-orange story-center" style="width: 300px; height: 200px; border-radius: 60px;">
136
+ Div 60px
137
+ </div>
138
+ </div>
139
+ `,
140
+ }),
141
+ };
142
+
143
+ export const Disabled = {
144
+ // name: 'Story Name',
145
+ // parameters: {
146
+ // customCSS: `.myclass { border: 4px solid red; }`,
147
+ // },
148
+ render: (args, { argTypes }) => ({
149
+ components: { PxCorners },
150
+ setup() {
151
+ return { args };
152
+ },
153
+ template: `<PxCorners :smoothingEnabled="false" v-bind="args" class="story-size-md story-bg-orange" :radius="40" :borderWidth="4" borderColor="black" />`,
154
+ }),
155
+ };
156
+
157
+ export const DisabledAutoFromStyle = {
158
+ // name: 'Story Name',
159
+ // parameters: {
160
+ // customCSS: `.myclass { border: 4px solid red; }`,
161
+ // },
162
+ render: (args, { argTypes }) => ({
163
+ components: { PxCorners },
164
+ setup() {
165
+ return { args };
166
+ },
167
+ template: `<PxCorners :smoothingEnabled="false" v-bind="args" class="story-size-md story-bg-orange" style="border-radius: 40px; border: 4px solid black;" />`,
168
+ }),
169
+ };
170
+
171
+ export const BasicMask = {
172
+ // name: 'Story Name',
173
+ // parameters: {
174
+ // customCSS: `.myclass { border: 4px solid red; }`,
175
+ // },
176
+ render: (args, { argTypes }) => ({
177
+ components: { PxCorners },
178
+ setup() {
179
+ return { args };
180
+ },
181
+ template: `<PxCorners v-bind="args" renderMode="mask" class="story-size-md story-bg-orange" :radius="40" :borderWidth="4" borderColor="black" />`,
182
+ }),
183
+ };
184
+ // export const Advanced = {
185
+ // ...Basic,
186
+ // // args: { ... },
187
+ // };
@@ -1,4 +1,5 @@
1
1
  @use '@thinkpixellab-public/px-styles/all' as *;
2
+ @use 'sass:color' as color;
2
3
 
3
4
  // Update this to test font inheritance
4
5
  .sbdocs {
@@ -91,11 +92,8 @@ body {
91
92
  }
92
93
 
93
94
  .story-gradient {
94
- background-image: repeating-linear-gradient(
95
- -45deg,
96
- rgba(black, 0.1) 0 20px,
97
- rgba(black, 0) 20px 40px
98
- ),
95
+ background-image:
96
+ repeating-linear-gradient(-45deg, rgba(black, 0.1) 0 20px, rgba(black, 0) 20px 40px),
99
97
  linear-gradient(135deg, tomato, purple);
100
98
  width: 100%;
101
99
  height: 100%;
@@ -145,6 +143,13 @@ body {
145
143
  background-color: gold;
146
144
  }
147
145
 
146
+ .story-hover-bg-blue {
147
+ cursor: pointer;
148
+ }
149
+ .story-hover-bg-blue:hover {
150
+ background-color: dodgerblue;
151
+ }
152
+
148
153
  .story-size-xs {
149
154
  width: 2em;
150
155
  height: 2em;
@@ -176,7 +181,7 @@ body {
176
181
  font-family: sans-serif,
177
182
  padding: 0.5em,
178
183
  font-size: 14px,
179
- background-color: mix(dodgerblue, white, 15%),
184
+ background-color: color.mix(dodgerblue, white, 15%),
180
185
  )
181
186
  );
182
187
  }
@@ -0,0 +1,395 @@
1
+ /**
2
+ * smootherCorners.js
3
+ *
4
+ * Utility functions for creating smooth, squircle-like corners using CSS clip-path.
5
+ * Based on Figma's corner smoothing approach.
6
+ *
7
+ * Usage:
8
+ * ```js
9
+ * import { getSmoothCornersPath } from '@thinkpixellab-public/px-vue/utils/smootherCorners'
10
+ *
11
+ * const clipPath = getSmoothCornersPath({
12
+ * width: 200,
13
+ * height: 100,
14
+ * radius: 16,
15
+ * smoothing: 0.8
16
+ * })
17
+ * ```
18
+ */
19
+
20
+ import { getSvgPath } from 'figma-squircle';
21
+
22
+ /**
23
+ * Parse percentage string to number
24
+ * @param {string|number} value - Value to parse
25
+ * @param {number} reference - Reference value for percentage calculation
26
+ * @returns {number}
27
+ */
28
+ export function parsePercentage(value, reference) {
29
+ if (typeof value === 'string' && value.endsWith('%')) {
30
+ const percent = parseFloat(value) / 100;
31
+ return reference * percent;
32
+ }
33
+ return parseFloat(value) || 0;
34
+ }
35
+
36
+ /**
37
+ * Expand radius array using CSS border-radius rules
38
+ * @param {number|Array} radius - Single radius or array of radii
39
+ * @returns {Array<number>} Array of four corner radii [tl, tr, br, bl]
40
+ */
41
+ export function expandRadiusArray(radius) {
42
+ if (!Array.isArray(radius)) {
43
+ return [radius, radius, radius, radius];
44
+ }
45
+
46
+ const [tl, tr = tl, br = tl, bl = tr] = radius;
47
+ return [tl, tr, br, bl];
48
+ }
49
+
50
+ /**
51
+ * Calculate corner radii with percentage support and clamping
52
+ * @param {number|Array|string} radius - Radius value(s)
53
+ * @param {number} width - Element width
54
+ * @param {number} height - Element height
55
+ * @param {number} minRadius - Minimum radius constraint
56
+ * @param {number} maxRadius - Maximum radius constraint
57
+ * @returns {Array<number>} Calculated radii for each corner
58
+ */
59
+ export function calculateCornerRadii(radius, width, height, minRadius = 0, maxRadius = Infinity) {
60
+ const reference = Math.max(width, height);
61
+ const expanded = expandRadiusArray(radius);
62
+
63
+ return expanded.map(r => {
64
+ const parsed = parsePercentage(r, reference);
65
+ return Math.max(minRadius, Math.min(maxRadius, parsed));
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Generate squircle path using figma-squircle library
71
+ * @param {number} width - Path width
72
+ * @param {number} height - Path height
73
+ * @param {Array<number>} radii - Corner radii [tl, tr, br, bl]
74
+ * @param {number} smoothing - Smoothing amount (0-1)
75
+ * @param {boolean} preserveSmoothing - Maintain smoothing at larger radii
76
+ * @returns {string} SVG path string
77
+ */
78
+ export function generateSquirclePath(
79
+ width,
80
+ height,
81
+ radii,
82
+ smoothing = 0.8,
83
+ preserveSmoothing = true,
84
+ ) {
85
+ if (width <= 0 || height <= 0) return '';
86
+
87
+ const [tl, tr, br, bl] = radii;
88
+
89
+ // check if all corners are the same (uniform)
90
+ const isUniform = tl === tr && tr === br && br === bl;
91
+
92
+ if (isUniform) {
93
+ // use simple API for uniform corners
94
+ return getSvgPath({
95
+ width,
96
+ height,
97
+ cornerRadius: tl,
98
+ cornerSmoothing: smoothing,
99
+ preserveSmoothing,
100
+ });
101
+ } else {
102
+ // use individual corner API for different corners
103
+ return getSvgPath({
104
+ width,
105
+ height,
106
+ cornerRadius: Math.max(tl, tr, br, bl), // fallback base radius
107
+ cornerSmoothing: smoothing,
108
+ preserveSmoothing,
109
+ topLeftCornerRadius: tl,
110
+ topRightCornerRadius: tr,
111
+ bottomRightCornerRadius: br,
112
+ bottomLeftCornerRadius: bl,
113
+ });
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Apply border offset to SVG path
119
+ * @param {string} path - SVG path string
120
+ * @param {number} offsetX - X offset
121
+ * @param {number} offsetY - Y offset
122
+ * @returns {string} Transformed path string
123
+ */
124
+ export function applyPathOffset(path, offsetX, offsetY) {
125
+ if (!path || (offsetX === 0 && offsetY === 0)) return path;
126
+
127
+ // transform path to account for offset
128
+ const translatedPath = path
129
+ .replace(/([ML])\s*([0-9.]+)\s+([0-9.]+)/g, (_, command, x, y) => {
130
+ return `${command} ${parseFloat(x) + offsetX} ${parseFloat(y) + offsetY}`;
131
+ })
132
+ .replace(
133
+ /([C])\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)/g,
134
+ (_, command, x1, y1, x2, y2, x3, y3) => {
135
+ return `${command} ${parseFloat(x1) + offsetX} ${parseFloat(y1) + offsetY} ${parseFloat(x2) + offsetX} ${parseFloat(y2) + offsetY} ${parseFloat(x3) + offsetX} ${parseFloat(y3) + offsetY}`;
136
+ },
137
+ );
138
+
139
+ return translatedPath;
140
+ }
141
+
142
+ /**
143
+ * Generate smooth corners clip-path value or SVG mask
144
+ * @param {Object} options - Configuration options
145
+ * @param {number} options.width - Element width
146
+ * @param {number} options.height - Element height
147
+ * @param {number|Array|string} options.radius - Corner radius (number, array, or percentage)
148
+ * @param {number} options.smoothing - Smoothing amount 0-1 (default: 0.8)
149
+ * @param {number} options.borderWidth - Border width to account for (default: 0)
150
+ * @param {number} options.minRadius - Min radius in px (default: 0)
151
+ * @param {number} options.maxRadius - Max radius in px (default: Infinity)
152
+ * @param {boolean} options.preserveSmoothing - Maintain smoothing at larger radii (default: true)
153
+ * @param {string} options.renderMode - Render mode: 'clip-path' | 'mask' (default: 'clip-path')
154
+ * @returns {string|null} CSS clip-path value, SVG mask data URL, or null if invalid dimensions
155
+ */
156
+ export function getSmoothCornersPath(options = {}) {
157
+ const {
158
+ width,
159
+ height,
160
+ radius = 8,
161
+ smoothing = 0.8,
162
+ borderWidth = 0,
163
+ minRadius = 0,
164
+ maxRadius = Infinity,
165
+ preserveSmoothing = true,
166
+ renderMode = 'clip-path',
167
+ } = options;
168
+
169
+ if (width <= 0 || height <= 0) return null;
170
+
171
+ const adjustedWidth = Math.max(0, width - borderWidth * 2);
172
+ const adjustedHeight = Math.max(0, height - borderWidth * 2);
173
+
174
+ if (adjustedWidth <= 0 || adjustedHeight <= 0) return null;
175
+
176
+ // calculate corner radii with percentage support and clamping
177
+ const radii = calculateCornerRadii(radius, adjustedWidth, adjustedHeight, minRadius, maxRadius);
178
+
179
+ // check if all radii are 0 - no need for clipping/masking
180
+ if (radii.every(r => r === 0)) {
181
+ return null;
182
+ }
183
+
184
+ // adjust radii for inner path when borderWidth is present
185
+ const adjustedRadii = borderWidth > 0
186
+ ? radii.map(r => Math.max(0, r - borderWidth))
187
+ : radii;
188
+
189
+ const path = generateSquirclePath(
190
+ adjustedWidth,
191
+ adjustedHeight,
192
+ adjustedRadii,
193
+ smoothing,
194
+ preserveSmoothing,
195
+ );
196
+
197
+ if (!path) return null;
198
+
199
+ // apply border offset if needed
200
+ const finalPath = borderWidth > 0 ? applyPathOffset(path, borderWidth, borderWidth) : path;
201
+
202
+ // return format based on render mode
203
+ if (renderMode === 'mask') {
204
+ return createSvgMaskDataUrl(finalPath, width, height);
205
+ } else {
206
+ return `path('${finalPath}')`;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Generate border mask using CSS masks instead of complex path manipulation
212
+ * @param {Object} options - Configuration options
213
+ * @returns {Object} Object with CSS mask properties for creating border effect
214
+ */
215
+ export function getBorderMaskStyles(options = {}) {
216
+ const { width, height, borderWidth = 0 } = options;
217
+
218
+ if (width <= 0 || height <= 0 || borderWidth <= 0) return null;
219
+
220
+ // Create the inner path (smaller for content area)
221
+ const innerPath = getSmoothCornersPath({
222
+ width: width - (borderWidth * 2),
223
+ height: height - (borderWidth * 2),
224
+ radius: options.radius,
225
+ smoothing: options.smoothing,
226
+ borderWidth: 0,
227
+ minRadius: options.minRadius,
228
+ maxRadius: options.maxRadius,
229
+ preserveSmoothing: options.preserveSmoothing,
230
+ });
231
+
232
+ if (!innerPath) return null;
233
+
234
+ // Position the inner mask to create border space
235
+ const maskPosition = `${borderWidth}px ${borderWidth}px`;
236
+ const maskSize = `${width - (borderWidth * 2)}px ${height - (borderWidth * 2)}px`;
237
+
238
+ return {
239
+ WebkitMask: `radial-gradient(white, white), ${innerPath}`,
240
+ mask: `radial-gradient(white, white), ${innerPath}`,
241
+ WebkitMaskComposite: 'xor',
242
+ maskComposite: 'subtract',
243
+ WebkitMaskPosition: `0 0, ${maskPosition}`,
244
+ maskPosition: `0 0, ${maskPosition}`,
245
+ WebkitMaskSize: `100% 100%, ${maskSize}`,
246
+ maskSize: `100% 100%, ${maskSize}`,
247
+ WebkitMaskRepeat: 'no-repeat, no-repeat',
248
+ maskRepeat: 'no-repeat, no-repeat'
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Generate border mask clip path using compound path approach or SVG mask
254
+ * @param {Object} options - Configuration options
255
+ * @param {string} options.renderMode - Render mode: 'clip-path' | 'mask' (default: 'clip-path')
256
+ * @returns {string|null} CSS clip-path value, SVG mask data URL, or null if invalid
257
+ */
258
+ export function getBorderMaskClipPath(options = {}) {
259
+ const { width, height, borderWidth = 0, renderMode = 'clip-path' } = options;
260
+
261
+ if (width <= 0 || height <= 0 || borderWidth <= 0) return null;
262
+
263
+ // Check if the border is too large for the element
264
+ if (borderWidth * 2 >= width || borderWidth * 2 >= height) return null;
265
+
266
+ // Create outer rectangle path (clockwise)
267
+ const rectPath = `M 0 0 H ${width} V ${height} H 0 Z`;
268
+
269
+ // Generate the inner smooth path directly at the correct size and position
270
+ const innerWidth = width - (borderWidth * 2);
271
+ const innerHeight = height - (borderWidth * 2);
272
+
273
+ // Calculate radii for the outer dimensions first
274
+ const outerRadii = calculateCornerRadii(
275
+ options.radius,
276
+ width,
277
+ height,
278
+ options.minRadius || 0,
279
+ options.maxRadius || Infinity
280
+ );
281
+
282
+ // check if all radii are 0 - no need for border masking
283
+ if (outerRadii.every(r => r === 0)) {
284
+ return null;
285
+ }
286
+
287
+ // Adjust radii for the inner path by subtracting border width
288
+ // This ensures consistent border thickness around corners
289
+ const innerRadii = outerRadii.map(r => Math.max(0, r - borderWidth));
290
+
291
+ // Generate the inner path with adjusted radii
292
+ const innerSmoothPath = generateSquirclePath(
293
+ innerWidth,
294
+ innerHeight,
295
+ innerRadii,
296
+ options.smoothing || 0.8,
297
+ options.preserveSmoothing !== false
298
+ );
299
+
300
+ if (!innerSmoothPath) return null;
301
+
302
+ // Apply offset to position the inner path
303
+ const offsetInnerPath = applyPathOffset(innerSmoothPath, borderWidth, borderWidth);
304
+
305
+ // Combine the paths
306
+ const combinedPath = `${rectPath} ${offsetInnerPath}`;
307
+
308
+ // Return format based on render mode
309
+ if (renderMode === 'mask') {
310
+ return createSvgBorderMaskDataUrl(combinedPath, width, height);
311
+ } else {
312
+ return `path(evenodd, '${combinedPath}')`;
313
+ }
314
+ }
315
+
316
+
317
+ /**
318
+ * Create SVG mask data URL from path string
319
+ * @param {string} pathString - SVG path string
320
+ * @param {number} width - Mask width
321
+ * @param {number} height - Mask height
322
+ * @returns {string} Data URL for SVG mask
323
+ */
324
+ export function createSvgMaskDataUrl(pathString, width, height) {
325
+ const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
326
+ <defs>
327
+ <mask id="smooth-mask">
328
+ <rect width="100%" height="100%" fill="black"/>
329
+ <path d="${pathString}" fill="white"/>
330
+ </mask>
331
+ </defs>
332
+ <rect width="100%" height="100%" fill="white" mask="url(#smooth-mask)"/>
333
+ </svg>`;
334
+
335
+ return `url('data:image/svg+xml;utf8,${encodeURIComponent(svgContent)}')`;
336
+ }
337
+
338
+ /**
339
+ * Create SVG border mask data URL using compound path approach
340
+ * @param {string} compoundPathString - Compound path string (outer rect + inner hole)
341
+ * @param {number} width - Mask width
342
+ * @param {number} height - Mask height
343
+ * @returns {string} Data URL for SVG border mask
344
+ */
345
+ export function createSvgBorderMaskDataUrl(compoundPathString, width, height) {
346
+ const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
347
+ <defs>
348
+ <mask id="border-mask">
349
+ <path d="${compoundPathString}" fill="white" fill-rule="evenodd"/>
350
+ </mask>
351
+ </defs>
352
+ <rect width="100%" height="100%" fill="white" mask="url(#border-mask)"/>
353
+ </svg>`;
354
+
355
+ return `url('data:image/svg+xml;utf8,${encodeURIComponent(svgContent)}')`;
356
+ }
357
+
358
+ /**
359
+ * Generate smooth corners configuration object
360
+ * @param {Object} options - Configuration options (same as getSmoothCornersPath)
361
+ * @returns {Object} Configuration object with computed values
362
+ */
363
+ export function generateSmoothCornersConfig(options = {}) {
364
+ const {
365
+ width,
366
+ height,
367
+ radius = 8,
368
+ smoothing = 0.8,
369
+ borderWidth = 0,
370
+ minRadius = 0,
371
+ maxRadius = Infinity,
372
+ preserveSmoothing = true,
373
+ } = options;
374
+
375
+ const adjustedWidth = Math.max(0, width - borderWidth * 2);
376
+ const adjustedHeight = Math.max(0, height - borderWidth * 2);
377
+
378
+ const radii = calculateCornerRadii(radius, adjustedWidth, adjustedHeight, minRadius, maxRadius);
379
+
380
+ // adjust radii for inner path when borderWidth is present
381
+ const adjustedRadii = borderWidth > 0
382
+ ? radii.map(r => Math.max(0, r - borderWidth))
383
+ : radii;
384
+
385
+ return {
386
+ width: adjustedWidth,
387
+ height: adjustedHeight,
388
+ radii: adjustedRadii,
389
+ originalRadii: radii,
390
+ smoothing,
391
+ preserveSmoothing,
392
+ borderWidth,
393
+ clipPath: getSmoothCornersPath(options),
394
+ };
395
+ }