poe-svelte-ui-lib 1.4.7 → 1.5.0

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.
@@ -15,7 +15,7 @@
15
15
  onChange = () => {},
16
16
  }: IFileAttachProps = $props()
17
17
 
18
- let ID = `${id}-${crypto.randomUUID().slice(0, 6)}`
18
+ let ID = $derived(`${id}-${crypto.randomUUID().slice(0, 6)}`)
19
19
  let selectedFile = $state<File | null>(null)
20
20
  let previewUrl = $derived(currentImage ? (currentImage.startsWith('data:') ? currentImage : `data:image/png;base64,${currentImage}`) : null)
21
21
  let fileName = $state('')
@@ -31,15 +31,6 @@
31
31
  <div class="relative flex flex-row items-start justify-center">
32
32
  <!-- Сообщение для отправки в ws по нажатию кнопки -->
33
33
  <div class="flex w-1/3 flex-col items-center px-2">
34
- <UI.Select
35
- label={{ name: $t('constructor.props.variable') }}
36
- options={VARIABLE_OPTIONS}
37
- value={VARIABLE_OPTIONS.find((opt) => opt.value === component.properties.id)}
38
- onUpdate={(value) => {
39
- updateProperty('id', value.value as string, component, onPropertyChange)
40
- onPropertyChange({ name: value.name?.split('—')[1].trim(), eventHandler: { Variables: value.value as string } })
41
- }}
42
- />
43
34
  <UI.Select
44
35
  label={{ name: $t('constructor.props.access') }}
45
36
  type="buttons"
@@ -66,7 +66,7 @@
66
66
  class={twMerge(
67
67
  `w-full rounded-2xl border px-4 py-1 text-center shadow-[0_0_3px_rgb(0_0_0_/0.25)] transition duration-200
68
68
  outline-none focus:shadow-[0_0_6px_var(--blue-color)] focus:border-(--blue-color) [&::-webkit-inner-spin-button]:hidden [&::-webkit-outer-spin-button]:hidden
69
- ${isValid ? 'border-(--bg-color)' : 'border-red-400 shadow-[0_0_6px_var(--red-color)] focus:border-red-400'}
69
+ ${isValid ? 'border-(--bg-color)' : 'border-red-400 shadow-[0_0_6px_var(--red-color)] focus:shadow-[0_0_6px_var(--red-color)] focus:border-red-400'}
70
70
  ${disabled ? 'opacity-50' : 'hover:shadow-[0_0_6px_rgb(0_0_0_/0.25)]'}
71
71
  ${readonly ? '' : 'hover:shadow-[0_0_6px_rgb(0_0_0_/0.25)]'}
72
72
  ${help?.info ? 'pl-8' : ''}
@@ -92,7 +92,7 @@
92
92
  class={twMerge(
93
93
  `h-full w-full resize-y rounded-2xl border border-(--border-color) px-2 py-1 text-center shadow-[0_0_3px_rgb(0_0_0_/0.25)] transition
94
94
  duration-200 outline-none focus:border-blue-400
95
- ${isValid ? 'border-(--bg-color)' : 'border-red-400 shadow-[0_0_6px_var(--red-color)]'}
95
+ ${isValid ? 'border-(--bg-color)' : 'border-red-400 shadow-[0_0_6px_var(--red-color)] focus:shadow-[0_0_6px_var(--red-color)] focus:border-red-400'}
96
96
  ${disabled ? 'cursor-not-allowed opacity-50' : 'hover:shadow-[0_0_6px_rgb(0_0_0_/0.25)]'}
97
97
  ${readonly ? '' : 'hover:shadow-[0_0_6px_rgb(0_0_0_/0.25)]'}
98
98
  ${help?.info ? 'pl-8' : ''}
@@ -112,7 +112,11 @@
112
112
  options={$optionsStore.INPUT_TYPE_OPTIONS}
113
113
  type="buttons"
114
114
  value={$optionsStore.INPUT_TYPE_OPTIONS.find((opt) => opt.value === (component.properties.type || 'text'))}
115
- onUpdate={(selectedOption) => updateProperty('type', selectedOption.value as string)}
115
+ onUpdate={(selectedOption) => {
116
+ updateProperty('type', selectedOption.value as string)
117
+ if (selectedOption.value === 'text-area') updateProperty('componentClass', twMerge(component.properties.componentClass, 'font-mono'))
118
+ else updateProperty('componentClass', twMerge(component.properties.componentClass, 'font-[Montserrat]'))
119
+ }}
116
120
  />
117
121
  {#if component.properties.type === 'text' || component.properties.type === 'password' || component.properties.type === 'text-area'}
118
122
  <UI.Input
@@ -235,7 +239,7 @@
235
239
  <UI.Input
236
240
  label={{ name: $t('constructor.props.componentclass') }}
237
241
  value={component.properties.componentClass}
238
- onUpdate={(value) => updateProperty('componentClass', value as string)}
242
+ onUpdate={(value) => updateProperty('componentClass', twMerge(component.properties.componentClass, value as string))}
239
243
  />
240
244
  <UI.Select
241
245
  wrapperClass="h-14"
@@ -99,7 +99,7 @@
99
99
  id={`${id}-${crypto.randomUUID().slice(0, 6)}`}
100
100
  value={value?.value ? String(value.value) : ''}
101
101
  class={twMerge(
102
- `w-full rounded-2xl border border-(--bg-color) p-1 text-center shadow-[0_0_3px_rgb(0_0_0_/0.25)] transition-shadow duration-200
102
+ `w-full rounded-2xl border border-(--bg-color) p-1 text-center shadow-[0_0_3px_rgb(0_0_0_/0.25)] transition duration-200
103
103
  ${disabled ? 'opacity-50' : 'cursor-pointer hover:shadow-[0_0_6px_rgb(0_0_0_/0.25)]'}`,
104
104
  value?.class,
105
105
  )}
@@ -143,7 +143,7 @@
143
143
  <button
144
144
  id={option.id}
145
145
  class="{twMerge(
146
- `m-0 inline-block min-w-0 flex-1 items-center px-2 py-1 font-semibold shadow-[0_0_3px_rgb(0_0_0_/0.25)] transition-all duration-300 select-none border border-(--bg-color)
146
+ `m-0 inline-block min-w-0 flex-1 items-center px-2 py-1 font-semibold shadow-[0_0_3px_rgb(0_0_0_/0.25)] transition duration-300 select-none border border-(--bg-color)
147
147
  ${option.disabled || disabled ? 'opacity-50' : 'cursor-pointer hover:shadow-[0_0_6px_rgb(0_0_0_/0.25)]'}
148
148
  ${option.value === value?.value && value !== null ? 'z-10 py-1 shadow-[0_0_10px_var(--shadow-color)] hover:shadow-[0_0_15px_var(--shadow-color)]' : ''}
149
149
  ${options.length > 0 && index === 0 ? 'rounded-l-2xl' : ''} ${index === options.length - 1 ? 'rounded-r-2xl' : ''}`,
@@ -166,7 +166,7 @@
166
166
  <input
167
167
  bind:value={searchValue}
168
168
  class="w-full appearance-none rounded-2xl border px-4 py-1 text-center shadow-[0_0_3px_rgb(0_0_0_/0.25)]
169
- transition-shadow duration-200 outline-none focus:shadow-[0_0_6px_var(--blue-color)]
169
+ transition duration-200 outline-none focus:shadow-[0_0_6px_var(--blue-color)]
170
170
  [&::-webkit-inner-spin-button]:hidden [&::-webkit-outer-spin-button]:hidden
171
171
  {disabled
172
172
  ? 'cursor-not-allowed opacity-50'
@@ -17,8 +17,8 @@
17
17
 
18
18
  const isRange = $derived(type === 'range' || (Array.isArray(value) && value.length === 2))
19
19
 
20
- const maxDigits = String(number.maxNum ?? 100).length
21
- const valueWidth = `${maxDigits + 1}ch` /* +1 на запас */
20
+ const maxDigits = $derived(String(number.maxNum ?? 100).length)
21
+ const valueWidth = $derived(`${maxDigits + 1}ch`) /* +1 на запас */
22
22
 
23
23
  /* Инициализация значений с проверкой типа */
24
24
  let singleValue = $derived(!isRange && typeof value === 'number' ? value : number.minNum)
@@ -10,9 +10,12 @@
10
10
  id = crypto.randomUUID(),
11
11
  wrapperClass = '',
12
12
  label = { name: '', class: '' },
13
- body = [],
13
+ body = $bindable(),
14
14
  header = [],
15
15
  footer = '',
16
+ stashData = false,
17
+ type = 'table',
18
+ rowsAmmount = 10,
16
19
  outline = false,
17
20
  cursor = null,
18
21
  loader,
@@ -22,6 +25,8 @@
22
25
  onClick,
23
26
  }: ITableProps<any> = $props()
24
27
 
28
+ let dataBuffer: any[] = $state([])
29
+
25
30
  /* Сортировка */
26
31
  let sortState: {
27
32
  key: string | null
@@ -33,6 +38,13 @@
33
38
 
34
39
  let isAutoscroll = $state(false)
35
40
 
41
+ const logTypeOptions = [
42
+ { id: crypto.randomUUID(), name: 'Error', value: 'error', color: 'bg-(--red-color)' },
43
+ { id: crypto.randomUUID(), name: 'Warning', value: 'warning', color: 'bg-(--yellow-color)' },
44
+ { id: crypto.randomUUID(), name: 'Info', value: 'info', color: 'bg-(--gray-color)' },
45
+ ]
46
+ let logType = $state(['error', 'info'])
47
+
36
48
  /* Сортировка столбцов */
37
49
  const sortRows = (key: string) => {
38
50
  if (sortState.key === key) {
@@ -87,8 +99,9 @@
87
99
  container.scrollTop = container.scrollHeight
88
100
  }
89
101
  }
102
+
90
103
  $effect(() => {
91
- if (body.length > 0) {
104
+ if (autoscroll && dataBuffer && dataBuffer.length > 0) {
92
105
  scrollToBottom()
93
106
  }
94
107
  })
@@ -141,12 +154,78 @@
141
154
  return !!src
142
155
  }
143
156
 
157
+ $effect(() => {
158
+ if (body && type == 'logger') {
159
+ dataBuffer = [
160
+ ...dataBuffer,
161
+ {
162
+ type: Object.entries(body)[0][1] as string,
163
+ color: `<div class='size-6 rounded-full ${logTypeOptions.find((o) => o.value == body.logLevel)?.color}'></div>`,
164
+ data: Object.entries(body)[1][1] as string,
165
+ },
166
+ ]
167
+
168
+ if (dataBuffer.length > rowsAmmount * 5) {
169
+ dataBuffer = dataBuffer.slice(-(rowsAmmount * 5))
170
+ }
171
+
172
+ body = null
173
+ }
174
+ })
175
+
176
+ $effect(() => {
177
+ if (body && stashData && type == 'table') {
178
+ dataBuffer = [...dataBuffer, body]
179
+ if (dataBuffer.length > rowsAmmount) {
180
+ dataBuffer = dataBuffer.slice(-rowsAmmount)
181
+ }
182
+
183
+ body = null
184
+ }
185
+ })
186
+
187
+ $effect(() => {
188
+ const currentType = type
189
+ if (type === 'logger') {
190
+ header = [
191
+ {
192
+ key: 'color',
193
+ label: { name: 'Type' },
194
+ width: '3rem',
195
+ } as ITableHeader<any>,
196
+ {
197
+ key: 'data',
198
+ label: { name: 'Data' },
199
+ width: 'calc(100% - 3rem)',
200
+ } as ITableHeader<any>,
201
+ ]
202
+ }
203
+ return () => {
204
+ dataBuffer = []
205
+ }
206
+ })
207
+
144
208
  onMount(() => {
145
209
  if (autoscroll) {
146
210
  container?.addEventListener('scroll', handleAutoScroll)
147
211
  scrollToBottom()
148
212
  }
149
213
 
214
+ if (type === 'logger') {
215
+ header = [
216
+ {
217
+ key: 'color',
218
+ label: { name: 'Type' },
219
+ width: '3rem',
220
+ } as ITableHeader<any>,
221
+ {
222
+ key: 'data',
223
+ label: { name: 'Data' },
224
+ width: 'calc(100% - 3rem)',
225
+ } as ITableHeader<any>,
226
+ ]
227
+ }
228
+
150
229
  return () => {
151
230
  if (autoscroll) {
152
231
  container?.removeEventListener('scroll', handleAutoScroll)
@@ -155,13 +234,51 @@
155
234
  })
156
235
  </script>
157
236
 
158
- <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class={twMerge(`bg-blue flex h-full w-full flex-col overflow-hidden`, wrapperClass)}>
237
+ <div
238
+ id={`${id}-${crypto.randomUUID().slice(0, 6)}`}
239
+ class={twMerge(`bg-blue flex h-full w-full gap-2 items-center flex-col overflow-hidden`, wrapperClass)}
240
+ >
159
241
  {#if label.name}
160
242
  <h5 class={twMerge(`w-full px-4 text-center`, label.class)}>{label.name}</h5>
161
243
  {/if}
162
244
 
245
+ {#if type == 'logger'}
246
+ <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class="flex w-[50%] justify-center rounded-full">
247
+ {#each logTypeOptions as option, index}
248
+ <button
249
+ id={crypto.randomUUID()}
250
+ class={twMerge(`m-0 inline-block min-w-0 flex-1 cursor-pointer items-center px-2 py-1 font-semibold shadow-sm transition-all duration-300
251
+ select-none hover:shadow-md
252
+ ${
253
+ logType.includes(option.value) && logType !== null
254
+ ? 'z-10 py-1 shadow-[0_0_10px_var(--shadow-color)] hover:shadow-[0_0_15px_var(--shadow-color)]'
255
+ : ''
256
+ }
257
+ ${logTypeOptions.length > 0 && index === 0 ? 'rounded-l-2xl' : ''} ${
258
+ index === logTypeOptions.length - 1 ? 'rounded-r-2xl' : ''
259
+ } ${option.color}`)}
260
+ onclick={() => {
261
+ if (logType.includes(option.value)) {
262
+ logType = logType.filter((type) => type !== option.value)
263
+ } else {
264
+ logType.push(option.value)
265
+ }
266
+ }}
267
+ >
268
+ <span class="flex flex-row items-center justify-center gap-4">
269
+ {#if option}
270
+ <div class="flex-1">
271
+ {option.name}
272
+ </div>
273
+ {/if}
274
+ </span>
275
+ </button>
276
+ {/each}
277
+ </div>
278
+ {/if}
279
+
163
280
  <div
164
- class="flex h-full flex-col overflow-hidden rounded-xl border shadow-sm transition duration-200 hover:shadow-md {outline
281
+ class="flex h-full w-full flex-col overflow-hidden rounded-xl border shadow-sm transition duration-200 hover:shadow-md {outline
165
282
  ? ' border-(--border-color)'
166
283
  : 'border-transparent'} "
167
284
  >
@@ -171,9 +288,9 @@
171
288
  <div
172
289
  class={twMerge(
173
290
  `items-center justify-center border-l ${outline && index !== 0 ? ' border-(--border-color)' : 'border-transparent'} ${
174
- column.align?.header === 'center'
291
+ column.align === 'center'
175
292
  ? 'flex justify-center text-center'
176
- : column.align?.header === 'right'
293
+ : column.align === 'right'
177
294
  ? 'flex justify-end text-right'
178
295
  : 'flex justify-start text-left'
179
296
  } gap-1 bg-(--bg-color) p-2 text-left`,
@@ -193,126 +310,129 @@
193
310
  {/each}
194
311
  </div>
195
312
 
196
- <!-- Table Body с прокруткой -->
197
- <div class="flex-1 overflow-y-auto bg-(--container-color)/50" bind:this={container} onscroll={handleScroll}>
198
- <div class="grid min-w-0" style={`grid-template-columns: ${header.map((c) => c.width || 'minmax(0, 1fr)').join(' ')};`}>
199
- {#each body as row, index (row)}
200
- {#each header as column, j (column)}
201
- <div
202
- class="relative flex w-full min-w-0 items-center px-2 py-1 wrap-break-word
313
+ {#if body || dataBuffer}
314
+ {@const rows = type == 'logger' ? dataBuffer.filter((str) => logType.includes(str.type)).slice(-rowsAmmount) : stashData ? dataBuffer : body}
315
+ <!-- Table Body с прокруткой -->
316
+ <div class="flex-1 overflow-y-auto bg-(--container-color)/50" bind:this={container} onscroll={handleScroll}>
317
+ <div class="grid min-w-0" style={`grid-template-columns: ${header.map((c) => c.width || 'minmax(0, 1fr)').join(' ')};`}>
318
+ {#each rows as row, index (row)}
319
+ {#each header as column, j (column)}
320
+ <div
321
+ class="relative flex w-full min-w-0 items-center px-2 py-1 wrap-break-word
203
322
  {index % 2 ? 'bg-(--back-color)/40' : 'bg-[#edeef3] dark:bg-[#1f2a3a]'}
204
- {column.align?.content === 'center'
205
- ? 'flex justify-center text-center'
206
- : column.align?.content === 'right'
207
- ? 'flex justify-end text-right'
208
- : 'flex justify-start text-left'}
323
+ {column.align === 'center'
324
+ ? 'flex justify-center text-center'
325
+ : column.align === 'right'
326
+ ? 'flex justify-end text-right'
327
+ : 'flex justify-start text-left'}
209
328
  border-t
210
329
  {j !== 0 ? ' border-l ' : ''}
211
330
  {outline ? 'border-(--border-color)' : 'border-transparent'} "
212
- >
213
- {#if column.buttons}
214
- <div class="flex w-full flex-col gap-1">
215
- {#each column.buttons as button (button)}
216
- <button
217
- class="{twMerge(`cursor-pointer rounded-full
331
+ >
332
+ {#if column.buttons}
333
+ <div class="flex w-full flex-col gap-1">
334
+ {#each column.buttons as button (button)}
335
+ <button
336
+ class="{twMerge(`cursor-pointer rounded-full
218
337
  px-4 py-1 font-semibold shadow-sm transition-shadow duration-200 outline-none select-none hover:shadow-md
219
338
  ${typeof button.class === 'function' ? button.class(row) : button.class}`)} bg-(--bg-color)"
220
- onclick={() => buttonClick(row, button)}
221
- >
222
- {typeof button.name === 'function' ? button.name(row) : button.name}
223
- </button>
224
- {/each}
225
- </div>
226
- {:else if column.image?.src || column.image?.defaultIcon}
227
- <div
228
- class="flex items-center justify-center"
229
- style={`width: ${column.image.width || '5rem'}; height: ${column.image.height || '5rem'};`}
230
- >
231
- {#if hasImage(column, row)}
232
- <img
233
- src={typeof column.image?.src === 'function' ? column.image.src(row) : column.image?.src || ''}
234
- alt={column.image.alt ?? 'Image'}
235
- class={twMerge(`h-full w-full object-cover ${column.image.class || ''}`)}
236
- loading="lazy"
237
- />
238
- {:else if column.image.defaultIcon}
239
- {#if typeof column.image.defaultIcon === 'string'}
240
- {@html column.image.defaultIcon}
339
+ onclick={() => buttonClick(row, button)}
340
+ >
341
+ {typeof button.name === 'function' ? button.name(row) : button.name}
342
+ </button>
343
+ {/each}
344
+ </div>
345
+ {:else if column.image?.src || column.image?.defaultIcon}
346
+ <div
347
+ class="flex items-center justify-center"
348
+ style={`width: ${column.image.width || '5rem'}; height: ${column.image.height || '5rem'};`}
349
+ >
350
+ {#if hasImage(column, row)}
351
+ <img
352
+ src={typeof column.image?.src === 'function' ? column.image.src(row) : column.image?.src || ''}
353
+ alt={column.image.alt ?? 'Image'}
354
+ class={twMerge(`h-full w-full object-cover ${column.image.class || ''}`)}
355
+ loading="lazy"
356
+ />
357
+ {:else if column.image.defaultIcon}
358
+ {#if typeof column.image.defaultIcon === 'string'}
359
+ {@html column.image.defaultIcon}
360
+ {:else}
361
+ <column.image.defaultIcon />
362
+ {/if}
363
+ {/if}
364
+ </div>
365
+ {:else}
366
+ <div
367
+ class=" w-full max-w-full wrap-break-word {column.overflow?.truncated ? 'truncate' : ' whitespace-normal'}"
368
+ onmouseenter={column.overflow?.truncated ? (e) => showTooltip(e, row[column.key], column.overflow?.formatting) : undefined}
369
+ onmouseleave={column.overflow?.truncated ? hideTooltip : undefined}
370
+ onmousemove={column.overflow?.truncated
371
+ ? (e) => {
372
+ tooltip.x = e.clientX
373
+ tooltip.y = e.clientY
374
+ }
375
+ : undefined}
376
+ role="columnheader"
377
+ tabindex={null}
378
+ >
379
+ {#if column.overflow?.modal}
380
+ <button
381
+ class="w-full cursor-pointer overflow-hidden text-left text-ellipsis whitespace-nowrap"
382
+ onclick={(e) => {
383
+ e.stopPropagation()
384
+ showModal(row[column.key], column.overflow?.formatting)
385
+ }}
386
+ >
387
+ {@html row[column.key]}
388
+ </button>
241
389
  {:else}
242
- <column.image.defaultIcon />
390
+ {@html row[column.key]}
243
391
  {/if}
244
- {/if}
245
- </div>
246
- {:else}
247
- <div
248
- class=" w-full max-w-full wrap-break-word {column.overflow?.truncated ? 'truncate' : ' whitespace-normal'}"
249
- onmouseenter={column.overflow?.truncated ? (e) => showTooltip(e, row[column.key], column.overflow?.formatting) : undefined}
250
- onmouseleave={column.overflow?.truncated ? hideTooltip : undefined}
251
- onmousemove={column.overflow?.truncated
252
- ? (e) => {
253
- tooltip.x = e.clientX
254
- tooltip.y = e.clientY
255
- }
256
- : undefined}
257
- role="columnheader"
258
- tabindex={null}
259
- >
260
- {#if column.overflow?.modal}
392
+ </div>
393
+ <!-- {#if column.overflow?.truncated}
394
+ <div class="whitespace-nowrap">{row[column.key].slice(-5)}</div>
395
+ {/if} -->
396
+
397
+ {#if column.overflow?.copy}
261
398
  <button
262
- class="w-full cursor-pointer overflow-hidden text-left text-ellipsis whitespace-nowrap"
399
+ class="mx-2 flex cursor-pointer border-none bg-transparent text-2xl"
263
400
  onclick={(e) => {
264
- e.stopPropagation()
265
- showModal(row[column.key], column.overflow?.formatting)
401
+ e.preventDefault()
402
+ navigator.clipboard.writeText(row[column.key])
403
+ copiedCell = { x: column.key as string, y: index }
404
+ setTimeout(() => (copiedCell = { x: '', y: -1 }), 1000)
266
405
  }}
406
+ aria-label="Копировать текст"
267
407
  >
268
- {@html row[column.key]}
408
+ <div class=" size-5 text-sm [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full">
409
+ {#if copiedCell.y === index && copiedCell.x === column.key}
410
+ <div
411
+ class="absolute top-1/2 right-3.5 -translate-y-1/2 transform rounded-md bg-(--green-color) px-1.5 py-1 shadow-lg"
412
+ transition:fade={{ duration: 200 }}
413
+ >
414
+
415
+ </div>
416
+ {:else}
417
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
418
+ <g fill="none" stroke="currentColor" stroke-width="1.5">
419
+ <path
420
+ d="M6 11c0-2.828 0-4.243.879-5.121C7.757 5 9.172 5 12 5h3c2.828 0 4.243 0 5.121.879C21 6.757 21 8.172 21 11v5c0 2.828 0 4.243-.879 5.121C19.243 22 17.828 22 15 22h-3c-2.828 0-4.243 0-5.121-.879C6 20.243 6 18.828 6 16z"
421
+ />
422
+ <path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3" />
423
+ </g>
424
+ </svg>
425
+ {/if}
426
+ </div>
269
427
  </button>
270
- {:else}
271
- {@html row[column.key]}
272
428
  {/if}
273
- </div>
274
- <!-- {#if column.overflow?.truncated}
275
- <div class="whitespace-nowrap">{row[column.key].slice(-5)}</div>
276
- {/if} -->
277
-
278
- {#if column.overflow?.copy}
279
- <button
280
- class="mx-2 flex cursor-pointer border-none bg-transparent text-2xl"
281
- onclick={(e) => {
282
- e.preventDefault()
283
- navigator.clipboard.writeText(row[column.key])
284
- copiedCell = { x: column.key as string, y: index }
285
- setTimeout(() => (copiedCell = { x: '', y: -1 }), 1000)
286
- }}
287
- aria-label="Копировать текст"
288
- >
289
- <div class=" size-5 text-sm [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full">
290
- {#if copiedCell.y === index && copiedCell.x === column.key}
291
- <div
292
- class="absolute top-1/2 right-3.5 -translate-y-1/2 transform rounded-md bg-(--green-color) px-1.5 py-1 shadow-lg"
293
- transition:fade={{ duration: 200 }}
294
- >
295
-
296
- </div>
297
- {:else}
298
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
299
- <g fill="none" stroke="currentColor" stroke-width="1.5">
300
- <path
301
- d="M6 11c0-2.828 0-4.243.879-5.121C7.757 5 9.172 5 12 5h3c2.828 0 4.243 0 5.121.879C21 6.757 21 8.172 21 11v5c0 2.828 0 4.243-.879 5.121C19.243 22 17.828 22 15 22h-3c-2.828 0-4.243 0-5.121-.879C6 20.243 6 18.828 6 16z"
302
- />
303
- <path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3" />
304
- </g>
305
- </svg>
306
- {/if}
307
- </div>
308
- </button>
309
429
  {/if}
310
- {/if}
311
- </div>
430
+ </div>
431
+ {/each}
312
432
  {/each}
313
- {/each}
433
+ </div>
314
434
  </div>
315
- </div>
435
+ {/if}
316
436
 
317
437
  {#if tooltip.show}
318
438
  <div
@@ -1,4 +1,4 @@
1
1
  import type { ITableProps } from '../types';
2
- declare const Table: import("svelte").Component<ITableProps<any>, {}, "modalData">;
2
+ declare const Table: import("svelte").Component<ITableProps<any>, {}, "body" | "modalData">;
3
3
  type Table = ReturnType<typeof Table>;
4
4
  export default Table;
@@ -24,7 +24,7 @@
24
24
  const DeviceVariables = getContext<{ id: string; value: string; name: string }[]>('DeviceVariables')
25
25
  let VARIABLE_OPTIONS = $derived(DeviceVariables && Array.isArray(DeviceVariables) ? DeviceVariables : [])
26
26
 
27
- let defaultIcon = $state({ isModalOpen: false, columnIndex: 0, column: component.properties.header[0] })
27
+ let defaultIcon = $derived({ isModalOpen: false, columnIndex: 0, column: component.properties.header ? component.properties.header[0] : '' })
28
28
 
29
29
  const initialColor = $derived(
30
30
  $optionsStore.COLOR_OPTIONS.find((c) =>
@@ -77,7 +77,7 @@
77
77
  </script>
78
78
 
79
79
  {#if forConstructor}
80
- <div class="relative flex flex-row items-start justify-center">
80
+ <div class="relative flex flex-row items-start justify-center pb-4">
81
81
  <div class="flex w-1/3 flex-col px-2">
82
82
  <UI.Select
83
83
  label={{ name: $t('constructor.props.variable') }}
@@ -95,6 +95,16 @@
95
95
  value={$optionsStore.ACCESS_OPTION.find((o) => o.value === component.access)}
96
96
  onUpdate={(option) => onPropertyChange({ access: option.value })}
97
97
  />
98
+ <UI.Select
99
+ label={{ name: $t('constructor.props.table.type') }}
100
+ type="buttons"
101
+ options={$optionsStore.TABLE_TYPE_OPTIONS}
102
+ value={$optionsStore.TABLE_TYPE_OPTIONS.find((o) => o.value === component.properties.type)}
103
+ onUpdate={(option) => {
104
+ updateProperty('type', option.value as string, component, onPropertyChange)
105
+ if (option.value === 'logger') updateProperty('stashData', true, component, onPropertyChange)
106
+ }}
107
+ />
98
108
  </div>
99
109
  <div class="flex w-1/3 flex-col px-2">
100
110
  <UI.Select
@@ -111,6 +121,14 @@
111
121
  options={[{ id: crypto.randomUUID(), value: 0, class: '' }]}
112
122
  onChange={(value) => updateProperty('outline', value, component, onPropertyChange)}
113
123
  />
124
+ <UI.Switch
125
+ label={{ name: $t('constructor.props.table.stashData') }}
126
+ value={component.properties.stashData}
127
+ options={[{ id: crypto.randomUUID(), value: 0, class: '', disabled: component.properties.type === 'logger' }]}
128
+ onChange={(value) => {
129
+ updateProperty('stashData', value, component, onPropertyChange)
130
+ }}
131
+ />
114
132
  </div>
115
133
  <div class="flex w-1/3 flex-col px-2">
116
134
  <UI.Input
@@ -125,6 +143,12 @@
125
143
  options={$optionsStore.TEXT_ALIGN_OPTIONS}
126
144
  onUpdate={(option) => updateProperty('label.class', twMerge(component.properties.label.class, option.value), component, onPropertyChange)}
127
145
  />
146
+ <UI.Input
147
+ label={{ name: $t('constructor.props.table.buffersize') }}
148
+ type="number"
149
+ value={component.properties.rowsAmmount}
150
+ onUpdate={(value) => updateProperty('rowsAmmount', value as string, component, onPropertyChange)}
151
+ />
128
152
  </div>
129
153
  </div>
130
154
 
@@ -183,11 +207,9 @@
183
207
  <UI.Select
184
208
  label={{ name: $t('constructor.props.align.content') }}
185
209
  type="buttons"
186
- value={$optionsStore.ALIGN_OPTIONS.find((a) => (a.value as string).includes(column.align?.content) || 'left')}
210
+ value={$optionsStore.ALIGN_OPTIONS.find((a) => (a.value as string).includes(column.align))}
187
211
  options={$optionsStore.ALIGN_OPTIONS}
188
- onUpdate={(option) => {
189
- updateTableHeader(columnIndex, 'align', { header: option.value, content: option.value })
190
- }}
212
+ onUpdate={(option) => updateTableHeader(columnIndex, 'align', option.value)}
191
213
  />
192
214
  <UI.Switch
193
215
  wrapperClass="w-30"
@@ -288,7 +310,7 @@
288
310
  {/each}
289
311
  </div>
290
312
  {:else}
291
- <div class="relative flex flex-row items-start justify-center">
313
+ <div class="relative flex flex-row items-start justify-center pb-4">
292
314
  <div class="flex w-1/3 flex-col px-2">
293
315
  <UI.Input
294
316
  label={{ name: $t('constructor.props.id') }}
@@ -407,13 +429,6 @@
407
429
  value={Number(column.width.replace('%', ''))}
408
430
  onUpdate={(value) => updateTableHeader(columnIndex, 'width', `${value}%`)}
409
431
  />
410
- <UI.Select
411
- label={{ name: $t('constructor.props.align.header') }}
412
- type="buttons"
413
- value={$optionsStore.ALIGN_OPTIONS.find((a) => (a.value as string).includes(column.align?.header) || 'left')}
414
- options={$optionsStore.ALIGN_OPTIONS}
415
- onUpdate={(option) => updateTableHeader(columnIndex, 'align', { header: option.value, content: column.align?.content })}
416
- />
417
432
  <UI.Switch
418
433
  label={{ name: $t('constructor.props.table.columns.sortable'), class: 'px-0' }}
419
434
  wrapperClass="w-30"
@@ -459,9 +474,9 @@
459
474
  <UI.Select
460
475
  label={{ name: $t('constructor.props.align.content') }}
461
476
  type="buttons"
462
- value={$optionsStore.ALIGN_OPTIONS.find((a) => (a.value as string).includes(column.align?.content) || 'left')}
477
+ value={$optionsStore.ALIGN_OPTIONS.find((a) => (a.value as string).includes(column.align))}
463
478
  options={$optionsStore.ALIGN_OPTIONS}
464
- onUpdate={(option) => updateTableHeader(columnIndex, 'align', { header: column.align?.header, content: option.value })}
479
+ onUpdate={(option) => updateTableHeader(columnIndex, 'align', option.value)}
465
480
  />
466
481
  <UI.Switch
467
482
  wrapperClass="w-2/10"
@@ -23,7 +23,7 @@
23
23
 
24
24
  const isCol = $derived(!!items.find((item) => item.class?.startsWith('flex-col')))
25
25
 
26
- let currentTabIndex: number = $state(activeTab)
26
+ let currentTabIndex: number = $derived(activeTab)
27
27
  </script>
28
28
 
29
29
  <div id={`${id}-${crypto.randomUUID().slice(0, 6)}`} class="w-full rounded-2xl bg-(--back-color)">
@@ -132,6 +132,9 @@ const translations = {
132
132
  'constructor.props.joystick.axes.info': 'Поле для ввода названий осей, разделенных пробелами (2 или 3 названия)',
133
133
  'constructor.props.file.select': 'Выберите файл',
134
134
  'constructor.props.file.notselected': 'Файл не выбран',
135
+ 'constructor.props.table.type': 'Тип таблицы',
136
+ 'constructor.props.table.type.table': 'Статическая таблица',
137
+ 'constructor.props.table.type.logger': 'Таблица для логов',
135
138
  'constructor.props.table.columns': 'Колонки таблицы',
136
139
  'constructor.props.table.columns.key': 'Ключ',
137
140
  'constructor.props.table.columns.label': 'Название колонки',
@@ -148,6 +151,8 @@ const translations = {
148
151
  'constructor.props.table.addaction': 'Добавить кнопку',
149
152
  'constructor.props.table.keys': 'Перечень ключей',
150
153
  'constructor.props.table.keys.info': 'Ключи таблицы, значения которых будут возвращаться',
154
+ 'constructor.props.table.stashData': 'Накопление данных',
155
+ 'constructor.props.table.buffersize': 'Размер буфера',
151
156
  'constructor.props.icon.access': 'Доступ',
152
157
  'constructor.props.icon.common': 'Общее',
153
158
  'constructor.props.icon.scenarios': 'Сценарии',
package/dist/options.d.ts CHANGED
@@ -115,6 +115,11 @@ export declare const optionsStore: import("svelte/store").Readable<{
115
115
  value: string;
116
116
  name: string;
117
117
  }[];
118
+ TABLE_TYPE_OPTIONS: {
119
+ id: string;
120
+ value: string;
121
+ name: string;
122
+ }[];
118
123
  AUTOCOMPLETE_CONSTRUCTOR_OPTIONS: {
119
124
  id: string;
120
125
  value: string;
package/dist/options.js CHANGED
@@ -124,6 +124,10 @@ export const optionsStore = derived(t, ($t) => {
124
124
  { id: id(), value: 'square', name: $t('constructor.props.type.square') },
125
125
  { id: id(), value: 'circle', name: $t('constructor.props.type.circle') },
126
126
  ],
127
+ TABLE_TYPE_OPTIONS: [
128
+ { id: id(), value: 'table', name: $t('constructor.props.table.type.table') },
129
+ { id: id(), value: 'logger', name: $t('constructor.props.table.type.logger') },
130
+ ],
127
131
  AUTOCOMPLETE_CONSTRUCTOR_OPTIONS: [
128
132
  { id: id(), value: 'on', name: $t('constructor.props.autocomplete.on') },
129
133
  { id: id(), value: 'off', name: $t('constructor.props.autocomplete.off') },
package/dist/types.d.ts CHANGED
@@ -34,10 +34,10 @@ export interface UIComponent {
34
34
  eventHandler?: IUIComponentHandler;
35
35
  }
36
36
  export interface Position {
37
- row: number;
38
- col: number;
39
- width: number;
40
- height: number;
37
+ row?: number;
38
+ col?: number;
39
+ width?: number;
40
+ height?: number;
41
41
  }
42
42
  export interface IUIComponentHandler {
43
43
  Header?: string;
@@ -233,10 +233,7 @@ export interface ITableHeader<T extends object> {
233
233
  key: keyof T;
234
234
  sortable?: boolean;
235
235
  width?: string;
236
- align?: {
237
- header?: 'left' | 'center' | 'right';
238
- content?: 'left' | 'center' | 'right';
239
- };
236
+ align?: 'left' | 'center' | 'right';
240
237
  overflow?: {
241
238
  truncated?: boolean;
242
239
  formatting?: (text: string) => string;
@@ -265,9 +262,12 @@ export interface ITableProps<T extends object> {
265
262
  name?: string;
266
263
  class?: string;
267
264
  };
268
- header: ITableHeader<T>[];
269
- body: T[];
265
+ header?: ITableHeader<T>[];
266
+ body: T[] | T | null;
270
267
  footer?: string;
268
+ type?: 'table' | 'logger';
269
+ stashData?: boolean;
270
+ rowsAmmount?: number;
271
271
  outline?: boolean;
272
272
  cursor?: string | null;
273
273
  loader?: Writable<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.4.7",
3
+ "version": "1.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -44,12 +44,12 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@sveltejs/adapter-static": "^3.0.10",
47
- "@sveltejs/kit": "^2.49.0",
47
+ "@sveltejs/kit": "^2.49.1",
48
48
  "@sveltejs/package": "^2.5.7",
49
49
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
50
50
  "@types/node": "^24.10.1",
51
51
  "publint": "^0.3.15",
52
- "svelte": "^5.45.3",
52
+ "svelte": "^5.45.5",
53
53
  "svelte-preprocess": "^6.0.3",
54
54
  "vite": "^7.2.6",
55
55
  "vite-plugin-compression": "^0.5.1"