@thinkpixellab-public/px-vue 4.1.13 → 4.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "4.1.13",
3
+ "version": "4.1.15",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": {
6
6
  "name": "Pixel Lab"
@@ -39,37 +39,36 @@
39
39
  "vue-inline-svg": "^3.1.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@babel/core": "^7.26.7",
43
- "@chromatic-com/storybook": "^3.2.4",
42
+ "@babel/core": "^7.26.10",
43
+ "@chromatic-com/storybook": "^3.2.6",
44
44
  "@nuxt/kit": "^3.15.4",
45
- "@rushstack/eslint-patch": "^1.10.5",
46
- "@storybook/addon-essentials": "^8.5.3",
47
- "@storybook/addon-interactions": "^8.5.3",
48
- "@storybook/addon-links": "^8.5.3",
49
- "@storybook/addon-mdx-gfm": "^8.5.3",
50
- "@storybook/blocks": "^8.5.3",
51
- "@storybook/manager-api": "^8.5.3",
52
- "@storybook/test": "^8.5.3",
53
- "@storybook/theming": "^8.5.3",
54
- "@storybook/vue3": "^8.5.3",
55
- "@storybook/vue3-vite": "^8.5.3",
56
- "@vitejs/plugin-vue": "^5.2.1",
45
+ "@rushstack/eslint-patch": "^1.11.0",
46
+ "@storybook/addon-essentials": "^8.6.7",
47
+ "@storybook/addon-interactions": "^8.6.7",
48
+ "@storybook/addon-links": "^8.6.7",
49
+ "@storybook/addon-mdx-gfm": "^8.6.7",
50
+ "@storybook/blocks": "^8.6.7",
51
+ "@storybook/manager-api": "^8.6.7",
52
+ "@storybook/test": "^8.6.7",
53
+ "@storybook/theming": "^8.6.7",
54
+ "@storybook/vue3": "^8.6.7",
55
+ "@storybook/vue3-vite": "^8.6.7",
56
+ "@vitejs/plugin-vue": "^5.2.3",
57
57
  "@vue/eslint-config-prettier": "^10.2.0",
58
58
  "@vue/test-utils": "^2.4.6",
59
59
  "babel-eslint": "^10.1.0",
60
- "babel-loader": "^9.2.1",
61
- "chromatic": "^11.25.2",
62
- "eslint": "^9.19.0",
63
- "eslint-plugin-storybook": "^0.11.2",
60
+ "chromatic": "^11.27.0",
61
+ "eslint": "^9.22.0",
62
+ "eslint-plugin-storybook": "^0.11.6",
64
63
  "eslint-plugin-vue": "^9.32.0",
65
64
  "jsdom": "^26.0.0",
66
- "prettier": "^3.4.2",
65
+ "prettier": "^3.5.3",
67
66
  "sass": "^1.83.4",
68
- "storybook": "^8.5.3",
69
- "storybook-addon-pseudo-states": "^4.0.2",
67
+ "storybook": "^8.6.7",
68
+ "storybook-addon-pseudo-states": "^4.0.3",
70
69
  "storybook-dark-mode": "^4.0.2",
71
- "vite": "^6.0.11",
72
- "vitest": "^3.0.5",
70
+ "vite": "^6.2.2",
71
+ "vitest": "^3.0.9",
73
72
  "vue-loader": "^17.4.2"
74
73
  },
75
74
  "bugs": {
@@ -0,0 +1,351 @@
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 { ref } from 'vue';
6
+ import PxToolPanel from '@/components/PxToolPanel.vue';
7
+ import PxToggleButton from '@/components/PxToggleButton.vue';
8
+ import { extractArgTypes } from '@/storybook/sb-utils.js';
9
+
10
+ export default {
11
+ title: 'Components/PxToolPanel',
12
+ component: PxToolPanel,
13
+ argTypes: {
14
+ title: { control: 'text' },
15
+ allowResize: {
16
+ control: 'select',
17
+ options: ['N', 'S', 'E', 'W', 'NS', 'EW', 'NSEW', 'NEWS', 'SWEN'],
18
+ },
19
+ showHeader: { control: 'boolean' },
20
+ teleportToBody: { control: 'boolean' },
21
+ minWidth: { control: 'number' },
22
+ minHeight: { control: 'number' },
23
+ constrainingRect: { control: 'object' },
24
+ bounds: { control: 'object' },
25
+ },
26
+ };
27
+
28
+ const Basic = {
29
+ args: {
30
+ title: 'Basic Panel',
31
+ allowResize: 'NSEW',
32
+ showHeader: true,
33
+ bounds: { x: 50, y: 50, width: 300, height: 200 },
34
+ },
35
+ render: (args, { argTypes }) => ({
36
+ components: { PxToolPanel },
37
+ setup() {
38
+ return { args };
39
+ },
40
+ template: `
41
+ <div class="drag-container">
42
+ <PxToolPanel v-bind="args">
43
+ <div class="panel-content">
44
+ <p>Basic panel with all resize handles enabled</p>
45
+ </div>
46
+ </PxToolPanel>
47
+ </div>
48
+ `,
49
+ }),
50
+ };
51
+
52
+ export const WithCustomHeader = {
53
+ ...Basic,
54
+ args: {
55
+ ...Basic.args,
56
+ headerClass: 'custom-header',
57
+ },
58
+ render: (args, { argTypes }) => ({
59
+ components: { PxToolPanel },
60
+ setup() {
61
+ return { args };
62
+ },
63
+ template: `
64
+ <div class="drag-container">
65
+ <PxToolPanel v-bind="args">
66
+ <template #header="{ title }">
67
+ <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
68
+ <span>{{ title }}</span>
69
+ <button>⚙️</button>
70
+ </div>
71
+ </template>
72
+ <div class="panel-content">
73
+ <p>Panel with custom header template</p>
74
+ </div>
75
+ </PxToolPanel>
76
+ </div>
77
+ `,
78
+ }),
79
+ };
80
+
81
+ export const WithoutHeader = {
82
+ ...Basic,
83
+ args: {
84
+ ...Basic.args,
85
+ allowResize: 'NEWS',
86
+ showHeader: false,
87
+ },
88
+ };
89
+
90
+ export const AllResizeHandles = {
91
+ ...Basic,
92
+ args: {
93
+ ...Basic.args,
94
+ allowResize: 'NSEW',
95
+ title: 'All Resize Handles',
96
+ },
97
+ };
98
+
99
+ export const HorizontalResizeOnly = {
100
+ ...Basic,
101
+ args: {
102
+ ...Basic.args,
103
+ allowResize: 'EW',
104
+ title: 'Horizontal Resize Only',
105
+ },
106
+ };
107
+
108
+ export const VerticalResizeOnly = {
109
+ ...Basic,
110
+ args: {
111
+ ...Basic.args,
112
+ allowResize: 'NS',
113
+ title: 'Vertical Resize Only',
114
+ },
115
+ };
116
+
117
+ export const CustomResizeHandleWidth = {
118
+ ...Basic,
119
+ args: {
120
+ ...Basic.args,
121
+ allowResize: 'NSEW',
122
+ title: 'Custom Resize Handle Width',
123
+ },
124
+ parameters: {
125
+ customCSS: `
126
+ .drag-container { width: 100%; height: 800px; position: relative; border: 1px dashed gray; }
127
+ .custom-resize-handles .px-tool-panel {
128
+ --px-tool-panel-resize-width: 20px;
129
+ }
130
+ `,
131
+ },
132
+ render: (args, { argTypes }) => ({
133
+ components: { PxToolPanel },
134
+ setup() {
135
+ return { args };
136
+ },
137
+ template: `
138
+ <div class="drag-container custom-resize-handles">
139
+ <PxToolPanel v-bind="args">
140
+ <div class="panel-content">
141
+ <p>Panel with larger resize handles (20px)</p>
142
+ <p>The resize handles should be visibly larger than other stories.</p>
143
+ </div>
144
+ </PxToolPanel>
145
+ </div>
146
+ `,
147
+ }),
148
+ };
149
+
150
+ export const ConstrainedMovement = {
151
+ ...Basic,
152
+ args: {
153
+ ...Basic.args,
154
+ title: 'Constrained Panel',
155
+ constrainingRect: { x: 100, y: 100, width: 600, height: 400 },
156
+ bounds: { x: 150, y: 150, width: 300, height: 200 },
157
+ },
158
+ render: (args, { argTypes }) => ({
159
+ components: { PxToolPanel },
160
+ setup() {
161
+ return { args };
162
+ },
163
+ template: `
164
+ <div class="drag-container">
165
+ <!-- Visual representation of the constraining rect -->
166
+ <div
167
+ style="
168
+ position: absolute;
169
+ left: 100px;
170
+ top: 100px;
171
+ width: 600px;
172
+ height: 400px;
173
+ border: 2px dashed #666;
174
+ background: rgba(0,0,0,0.05);
175
+ "
176
+ ></div>
177
+ <PxToolPanel v-bind="args">
178
+ <div class="panel-content">
179
+ <p>This panel is constrained within the dashed rectangle</p>
180
+ </div>
181
+ </PxToolPanel>
182
+ </div>
183
+ `,
184
+ }),
185
+ };
186
+
187
+ export const MultiplePanels = {
188
+ ...Basic,
189
+ parameters: {
190
+ customCSS: `
191
+ .drag-container { width: min(100%, 800px); height: 800px; position: relative; border: 1px dashed gray; padding: 1em; }
192
+ `,
193
+ },
194
+ render: (args, { argTypes }) => ({
195
+ components: { PxToolPanel },
196
+ setup() {
197
+ return { args };
198
+ },
199
+ template: `
200
+ <div class="drag-container">
201
+ <PxToolPanel
202
+ v-bind="args"
203
+ title="Panel 1"
204
+ :bounds="{ x: 50, y: 50, width: 300, height: 200 }"
205
+ allowResize="NSEW"
206
+ >
207
+ <div class="panel-content">
208
+ <p>Try interacting with multiple panels</p>
209
+ </div>
210
+ </PxToolPanel>
211
+ <PxToolPanel
212
+ v-bind="args"
213
+ title="Panel 2"
214
+ :bounds="{ x: 400, y: 50, width: 300, height: 200 }"
215
+ allowResize="NSEW"
216
+ >
217
+ <div class="panel-content">
218
+ <p>Panels can be moved and resized independently</p>
219
+ </div>
220
+ </PxToolPanel>
221
+ </div>
222
+ `,
223
+ }),
224
+ };
225
+
226
+ export const TeleportToBody = {
227
+ ...Basic,
228
+ args: {
229
+ ...Basic.args,
230
+ teleportToBody: true,
231
+ title: 'Teleported Panel',
232
+ allowResize: 'NSEW',
233
+ },
234
+ render: (args, { argTypes }) => ({
235
+ components: { PxToolPanel, PxToggleButton },
236
+ setup() {
237
+ const showPanel = ref(false);
238
+ return { args, showPanel };
239
+ },
240
+ template: `
241
+ <div>
242
+ <div class="drag-container">
243
+ <p>Click the button below to show a panel that will be teleported to the body element.</p>
244
+ <p>This is the original container. When shown, the panel can move outside of it.</p>
245
+ <p>Panel visible: {{ showPanel }}</p>
246
+ <PxToggleButton v-model:checked="showPanel">
247
+ {{ showPanel ? 'Hide Panel' : 'Show Panel' }}
248
+ </PxToggleButton>
249
+ <PxToolPanel v-if="showPanel" v-bind="args">
250
+ <div class="panel-content">
251
+ <p>This panel is rendered directly in the body element.</p>
252
+ <p>You can drag it anywhere on the page!</p>
253
+ </div>
254
+ </PxToolPanel>
255
+ </div>
256
+ </div>
257
+ `,
258
+ }),
259
+ };
260
+
261
+ export const ContentConstrainedSize = {
262
+ ...Basic,
263
+ args: {
264
+ ...Basic.args,
265
+ allowResize: 'NSEW',
266
+ title: 'Content-Constrained Size',
267
+ bounds: { x: 100, y: 100, width: 200, height: 200 }, // Intentionally smaller than minWidth/minHeight
268
+ minWidth: 300,
269
+ minHeight: 300,
270
+ },
271
+ parameters: {
272
+ customCSS: `
273
+ .drag-container { width: min(100%, 800px); height: 800px; position: relative; border: 1px dashed gray; padding: 1em; }
274
+ .content-constrained {
275
+ padding: 1em;
276
+ }
277
+ .content-constrained p {
278
+ margin: 0.5em 0;
279
+ }
280
+ `,
281
+ },
282
+ render: (args, { argTypes }) => ({
283
+ components: { PxToolPanel },
284
+ setup() {
285
+ return { args };
286
+ },
287
+ template: `
288
+ <div class="drag-container">
289
+ <PxToolPanel v-bind="args">
290
+ <div class="panel-content content-constrained">
291
+ <p>Size-Constrained Panel</p>
292
+ <p>Try resizing the panel smaller than the minimum dimensions.</p>
293
+ </div>
294
+ </PxToolPanel>
295
+ </div>
296
+ `,
297
+ }),
298
+ };
299
+
300
+ export const AutoHeight = {
301
+ ...Basic,
302
+ args: {
303
+ ...Basic.args,
304
+ allowResize: 'EW', // Only allow horizontal resizing
305
+ title: 'Auto Height Panel',
306
+ bounds: {
307
+ x: 50,
308
+ y: 50,
309
+ width: 300, // Initial width
310
+ height: null, // null height for auto sizing
311
+ },
312
+ },
313
+ parameters: {
314
+ customCSS: `
315
+ .drag-container { width: min(100%, 800px); height: 800px; position: relative; border: 1px dashed gray; padding: 1em; }
316
+ .auto-height-content {
317
+ padding: 1em;
318
+ }
319
+ .auto-height-content p {
320
+ margin: 0.5em 0;
321
+ }
322
+ .spacer {
323
+ height: 20px;
324
+ }
325
+ `,
326
+ },
327
+ render: (args, { argTypes }) => ({
328
+ components: { PxToolPanel },
329
+ setup() {
330
+ return { args };
331
+ },
332
+ template: `
333
+ <div class="drag-container">
334
+ <PxToolPanel v-bind="args">
335
+ <div class="panel-content auto-height-content">
336
+ <p>This panel's height is determined by its content.</p>
337
+ <p>You can only resize it horizontally.</p>
338
+ <div class="spacer"></div>
339
+ <p>The panel will automatically grow...</p>
340
+ <div class="spacer"></div>
341
+ <p>...or shrink...</p>
342
+ <div class="spacer"></div>
343
+ <p>...to fit its content!</p>
344
+ <div class="spacer"></div>
345
+ <p>Try resizing horizontally - the height will stay fitted to content.</p>
346
+ </div>
347
+ </PxToolPanel>
348
+ </div>
349
+ `,
350
+ }),
351
+ };
@@ -58,11 +58,43 @@ function squeezeContainer(headline, originalHeight, bottomRange, topRange, lineA
58
58
  }
59
59
  }
60
60
 
61
+ function textElementIsMultipleLines(element) {
62
+ if (!element) {
63
+ console.warn('textElementIsMultipleLines called with no element');
64
+ return false;
65
+ }
66
+
67
+ // get font metrics with getComputedStyle
68
+ const computedStyle = window.getComputedStyle(element);
69
+ const fontSize = parseFloat(computedStyle.fontSize);
70
+ const lineHeight =
71
+ computedStyle.lineHeight === 'normal'
72
+ ? fontSize * 1.2
73
+ : parseFloat(computedStyle.lineHeight);
74
+
75
+ // calculate total vertical spacing
76
+ const padding = parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
77
+ const border =
78
+ parseFloat(computedStyle.borderTopWidth) + parseFloat(computedStyle.borderBottomWidth);
79
+
80
+ // calculate expected single line height including padding and borders
81
+ const expectedSingleLineHeight = lineHeight + padding + border;
82
+
83
+ // get actual element height
84
+ const actualHeight = element.offsetHeight;
85
+
86
+ // add a small buffer (20% of line height) to account for font variations
87
+ const buffer = lineHeight * 0.2;
88
+
89
+ // return true if actual height is significantly larger than expected single line height
90
+ return actualHeight > expectedSingleLineHeight + buffer;
91
+ }
92
+
61
93
  // Function to see if a headline is multiple lines we only want to break if the headline is multiple
62
94
  // lines. We achieve this by turning the first word into a span and then we compare the height of
63
95
  // that span to the height of the entire headline. If the headline is bigger than the span by 10px
64
96
  // we balance the headline.
65
- function textElementIsMultipleLines(element) {
97
+ function textElementIsMultipleLinesLEGACY(element) {
66
98
  if (!element) {
67
99
  console.warn('textElementIsMultipleLines called with no element');
68
100
  return false;
package/utils/utils.js CHANGED
@@ -270,23 +270,49 @@ export function filterObjectByKeys(obj, allowedKeys) {
270
270
  */
271
271
 
272
272
  export function deepMerge(target, source, overwrite = true) {
273
- if (typeof target !== 'object' || typeof source !== 'object') return source;
273
+ // Handle non-object source
274
+ if (!source || typeof source !== 'object') {
275
+ return source;
276
+ }
277
+
278
+ // Handle non-object target
279
+ if (!target || typeof target !== 'object') {
280
+ return source;
281
+ }
274
282
 
275
283
  for (const key in source) {
276
284
  if (source.hasOwnProperty(key)) {
285
+ const sourceValue = source[key];
286
+ const targetValue = target[key];
287
+
288
+ // Skip if source value is undefined and we're not overwriting
289
+ if (!overwrite && sourceValue === undefined) {
290
+ continue;
291
+ }
292
+
293
+ // Handle null values and arrays directly
294
+ if (sourceValue === null || targetValue === null || Array.isArray(sourceValue)) {
295
+ target[key] = sourceValue;
296
+ continue;
297
+ }
298
+
299
+ // Recursively merge objects
277
300
  if (
278
- typeof source[key] === 'object' &&
279
- !Array.isArray(source[key]) &&
280
- typeof target[key] === 'object'
301
+ typeof sourceValue === 'object' &&
302
+ typeof targetValue === 'object' &&
303
+ !Array.isArray(targetValue)
281
304
  ) {
282
- target[key] = deepMerge(target[key], source[key], overwrite); // Recursively merge objects
283
- } else if (overwrite || !(key in target)) {
284
- target[key] = source[key];
305
+ target[key] = deepMerge(targetValue, sourceValue, overwrite);
306
+ }
307
+ // Handle all other cases (primitives, functions, etc.)
308
+ else if (overwrite || !(key in target)) {
309
+ target[key] = sourceValue;
285
310
  }
286
311
  }
287
312
  }
288
313
  return target;
289
314
  }
315
+
290
316
  /**
291
317
  * Returns an object representing the current query string parameters.
292
318
  */
@@ -514,3 +540,33 @@ export function findScrollParent(node) {
514
540
 
515
541
  return document.scrollingElement || document.documentElement;
516
542
  }
543
+
544
+ /**
545
+ * Converts strings into an array of items compatible with PxBaseItems.
546
+ * Each string will be used as the id, label, and value of the resulting item.
547
+ * Can accept either an array of strings or multiple string arguments.
548
+ *
549
+ * @param {string[]|...string} strings - Either an array of strings or multiple string arguments
550
+ * @returns {Array<{id: string, label: string, value: string}>} Array of PxBaseItems-compatible objects
551
+ * @example
552
+ * // Using array argument
553
+ * const items1 = stringsToBaseItems(['Apple', 'Banana', 'Orange']);
554
+ * // Returns: [
555
+ * // { id: 'Apple', label: 'Apple', value: 'Apple' },
556
+ * // { id: 'Banana', label: 'Banana', value: 'Banana' },
557
+ * // { id: 'Orange', label: 'Orange', value: 'Orange' }
558
+ * // ]
559
+ *
560
+ * // Using rest parameters
561
+ * const items2 = stringsToBaseItems('Apple', 'Banana', 'Orange');
562
+ * // Returns the same result as above
563
+ */
564
+ export function stringsToBaseItems(...strings) {
565
+ // If first argument is an array, use it directly, otherwise use all arguments
566
+ const items = Array.isArray(strings[0]) ? strings[0] : strings;
567
+ return items.map(str => ({
568
+ id: str.toLowerCase().replace(/ /g, '-'),
569
+ label: str,
570
+ value: str.toLowerCase().replace(/ /g, '-'),
571
+ }));
572
+ }
@@ -0,0 +1,52 @@
1
+ import { deepMerge } from './utils.js';
2
+
3
+ describe('deepMerge', () => {
4
+ test('handles basic object merging', () => {
5
+ const target = { a: 1, b: 2 };
6
+ const source = { b: 3, c: 4 };
7
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: 3, c: 4 });
8
+ });
9
+
10
+ test('handles nested object merging', () => {
11
+ const target = { a: { x: 1, y: 2 }, b: 2 };
12
+ const source = { a: { y: 3, z: 4 }, c: 4 };
13
+ expect(deepMerge(target, source)).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 2, c: 4 });
14
+ });
15
+
16
+ test('handles null values in source', () => {
17
+ const target = { a: { x: 1 }, b: 2 };
18
+ const source = { a: null, c: 3 };
19
+ expect(deepMerge(target, source)).toEqual({ a: null, b: 2, c: 3 });
20
+ });
21
+
22
+ test('handles null values in target', () => {
23
+ const target = { a: null, b: 2 };
24
+ const source = { a: { x: 1 }, c: 3 };
25
+ expect(deepMerge(target, source)).toEqual({ a: { x: 1 }, b: 2, c: 3 });
26
+ });
27
+
28
+ test('handles primitive values in nested objects', () => {
29
+ const target = { a: { x: 1 }, b: { y: 2 } };
30
+ const source = { a: 'string', b: { z: 3 } };
31
+ expect(deepMerge(target, source)).toEqual({ a: 'string', b: { y: 2, z: 3 } });
32
+ });
33
+
34
+ test('handles arrays', () => {
35
+ const target = { a: [1, 2], b: 2 };
36
+ const source = { a: [3, 4], c: 3 };
37
+ expect(deepMerge(target, source)).toEqual({ a: [3, 4], b: 2, c: 3 });
38
+ });
39
+
40
+ test('handles undefined values', () => {
41
+ const target = { a: { x: 1 }, b: undefined };
42
+ const source = { a: { y: 2 }, b: { z: 3 } };
43
+ expect(deepMerge(target, source)).toEqual({ a: { x: 1, y: 2 }, b: { z: 3 } });
44
+ });
45
+
46
+ test('handles non-object inputs', () => {
47
+ expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
48
+ expect(deepMerge({ a: 1 }, null)).toEqual(null);
49
+ expect(deepMerge(undefined, { a: 1 })).toEqual({ a: 1 });
50
+ expect(deepMerge({ a: 1 }, undefined)).toEqual(undefined);
51
+ });
52
+ });
package/utils/utilsSsr.js CHANGED
@@ -10,19 +10,15 @@ export function getQueryParams() {
10
10
  return params;
11
11
  };
12
12
 
13
- let queryParams;
14
-
15
- let ctx;
16
-
17
- if (typeof window === 'undefined') {
18
- ctx = useSSRContext();
19
- }
20
-
21
- if (ctx) {
22
- queryParams = getQueryParams(ctx.url);
23
- } else {
13
+ let queryParams = {};
14
+ if (import.meta.server) {
15
+ const ctx = useSSRContext();
16
+ // there are some error situations where ctx is not available
17
+ if (ctx) {
18
+ queryParams = getQueryParams(ctx.url);
19
+ }
20
+ } else if (typeof window !== 'undefined') {
24
21
  const { search } = window.location;
25
-
26
22
  queryParams = getQueryParams(search);
27
23
  }
28
24