@14ch/svelte-ui 0.0.7 → 0.0.9

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.
@@ -167,7 +167,6 @@
167
167
  // Methods
168
168
  // =========================================================================
169
169
 
170
-
171
170
  const handleChange = (event?: Event): void => {
172
171
  // 空文字列の場合はそのまま処理
173
172
  if (localValue && !localValue.startsWith('#')) {
@@ -155,7 +155,6 @@
155
155
  let popupRef = $state<any>();
156
156
  let highlightedIndex = $state(-1);
157
157
  let isFocused = $state(false);
158
- let comboboxRef = $state<HTMLDivElement>();
159
158
 
160
159
  // 各要素のIDを生成
161
160
  const inputId = `${id}-input`;
@@ -479,9 +478,9 @@
479
478
  type="button"
480
479
  class="combobox__option"
481
480
  class:combobox__option--highlighted={index === highlightedIndex}
482
- class:combobox__option--selected={option === value}
481
+ class:combobox__option--selected={option === inputValue}
483
482
  role="option"
484
- aria-selected={option === value}
483
+ aria-selected={option === inputValue}
485
484
  onmousedown={(event) => {
486
485
  event?.preventDefault?.();
487
486
  event?.stopPropagation?.();
@@ -366,12 +366,23 @@
366
366
  }
367
367
  };
368
368
 
369
+ const getNormalizedRange = () => {
370
+ if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return null;
371
+
372
+ const startDate = dayjs(value.start).startOf('day');
373
+ const endDate = dayjs(value.end).startOf('day');
374
+
375
+ if (startDate.isSameOrBefore(endDate)) {
376
+ return { start: startDate, end: endDate };
377
+ }
378
+
379
+ return { start: endDate, end: startDate };
380
+ };
381
+
369
382
  const isSelected = (date: dayjs.Dayjs) => {
370
- if (mode === 'range' && value && 'start' in value && 'end' in value) {
371
- return (
372
- dayjs(date).isSameOrAfter(dayjs(value.start).startOf('day')) &&
373
- dayjs(date).isSameOrBefore(dayjs(value.end).startOf('day'))
374
- );
383
+ const range = getNormalizedRange();
384
+ if (range) {
385
+ return dayjs(date).isSameOrAfter(range.start) && dayjs(date).isSameOrBefore(range.end);
375
386
  } else if (mode === 'single' && value && value instanceof Date) {
376
387
  return dayjs(date).isSame(dayjs(value).startOf('day'));
377
388
  }
@@ -379,25 +390,29 @@
379
390
  };
380
391
 
381
392
  const isRangeStart = (date: dayjs.Dayjs) => {
382
- if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
393
+ const range = getNormalizedRange();
394
+ if (!range) return false;
383
395
  if (isRangePreviewActive) return false;
384
- return dayjs(date).isSame(dayjs(value.start).startOf('day'));
396
+ return dayjs(date).isSame(range.start);
385
397
  };
386
398
 
387
399
  const isRangeEnd = (date: dayjs.Dayjs) => {
388
- if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
400
+ const range = getNormalizedRange();
401
+ if (!range) return false;
389
402
  if (isRangePreviewActive) return false;
390
- return dayjs(date).isSame(dayjs(value.end).startOf('day'));
403
+ return dayjs(date).isSame(range.end);
391
404
  };
392
405
 
393
406
  const isRangeMiddle = (date: dayjs.Dayjs) => {
394
- if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
407
+ const range = getNormalizedRange();
408
+ if (!range) return false;
395
409
  if (isRangePreviewActive) return false;
396
410
  return isSelected(date) && !isRangeStart(date) && !isRangeEnd(date);
397
411
  };
398
412
 
399
413
  const isRangeSingle = (date: dayjs.Dayjs) => {
400
- if (mode !== 'range' || !value || !('start' in value && 'end' in value)) return false;
414
+ const range = getNormalizedRange();
415
+ if (!range) return false;
401
416
  if (isRangePreviewActive) return false;
402
417
  return isRangeStart(date) && isRangeEnd(date);
403
418
  };
@@ -115,7 +115,7 @@
115
115
  return styles.join('; ');
116
116
  });
117
117
 
118
- const dialogClasses = $derived([scrollable && 'scrollable'].filter(Boolean).join(' ')); // dialogクラス自体はcustomClassに含めない
118
+ const dialogClasses = $derived('');
119
119
 
120
120
  const ariaLabelledby = $derived(title ? 'dialog-title' : undefined);
121
121
  const ariaDescribedbyValue = $derived(
@@ -136,7 +136,7 @@
136
136
  customStyles={dialogStyles}
137
137
  id={id ? `${id}-modal` : undefined}
138
138
  >
139
- <div class="dialog {scrollable ? 'scrollable' : ''}">
139
+ <div class="dialog {scrollable ? 'dialog--scrollable' : ''}">
140
140
  {#if header || title}
141
141
  <div class="dialog__header">
142
142
  {#if header}
@@ -166,14 +166,15 @@
166
166
  </div>
167
167
  </Modal>
168
168
 
169
- <style>.dialog {
169
+ <style>@charset "UTF-8";
170
+ .dialog {
170
171
  display: flex;
171
172
  flex-direction: column;
172
173
  justify-content: stretch;
173
- max-height: calc(100dvh - 2em - 6px);
174
174
  border: var(--svelte-ui-dialog-border);
175
175
  border-radius: var(--svelte-ui-dialog-border-radius);
176
- overflow: hidden;
176
+ max-height: calc(100dvh - 2em - 6px);
177
+ overflow: auto;
177
178
  }
178
179
 
179
180
  .dialog__header {
@@ -183,7 +184,6 @@
183
184
  justify-content: stretch;
184
185
  min-height: var(--svelte-ui-dialog-header-height);
185
186
  padding: var(--svelte-ui-dialog-padding);
186
- margin-bottom: calc(0px - var(--svelte-ui-dialog-body-padding));
187
187
  }
188
188
  .dialog__header .dialog__title {
189
189
  flex-grow: 1;
@@ -214,15 +214,26 @@
214
214
  border-bottom: 1px solid var(--svelte-ui-border-weak-color);
215
215
  }
216
216
 
217
- :global(.scrollable) .dialog__header {
217
+ /* scrollable=true 時: ヘッダー/フッター固定 + bodyのみスクロール */
218
+ .dialog--scrollable {
219
+ overflow: hidden;
220
+ }
221
+
222
+ .dialog--scrollable .dialog__header {
218
223
  margin-bottom: 0;
219
224
  border-bottom: solid var(--svelte-ui-border-width, 1px) var(--svelte-ui-border-weak-color);
220
225
  }
221
- :global(.scrollable) .dialog__body {
226
+
227
+ .dialog--scrollable .dialog__body {
222
228
  flex-shrink: 1;
223
229
  padding: var(--svelte-ui-dialog-body-padding);
224
230
  overflow: auto;
225
231
  }
226
- :global(.scrollable) .dialog__footer {
232
+
233
+ .dialog:not(.dialog--scrollable) .dialog__body {
234
+ padding-top: 0;
235
+ }
236
+
237
+ .dialog--scrollable .dialog__footer {
227
238
  border-top: solid var(--svelte-ui-border-width, 1px) var(--svelte-ui-border-weak-color);
228
239
  }</style>
@@ -123,9 +123,7 @@
123
123
  });
124
124
 
125
125
  const drawerClasses = $derived(
126
- ['drawer-wrapper', `drawer-wrapper--${position}`, scrollable && 'drawer-wrapper--scrollable']
127
- .filter(Boolean)
128
- .join(' ')
126
+ ['drawer-wrapper', `drawer-wrapper--${position}`].filter(Boolean).join(' ')
129
127
  );
130
128
 
131
129
  const ariaLabelledby = $derived(title ? 'drawer-title' : undefined);
@@ -7,6 +7,7 @@
7
7
  import type { HTMLInputAttributes } from 'svelte/elements';
8
8
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
9
9
  import { t } from '../i18n';
10
+ import { convertToHtmlWithLink } from '../utils/formatText';
10
11
 
11
12
  // =========================================================================
12
13
  // Props, States & Constants
@@ -58,6 +59,7 @@
58
59
  required = false,
59
60
  clearable = false,
60
61
  clearButtonAriaLabel = t('input.clear'),
62
+ linkify = false,
61
63
 
62
64
  // フォーカスイベント
63
65
  onfocus = () => {}, // No params for type inference
@@ -148,6 +150,7 @@
148
150
  readonly?: boolean;
149
151
  required?: boolean;
150
152
  clearable?: boolean;
153
+ linkify?: boolean;
151
154
 
152
155
  // フォーカスイベント
153
156
  onfocus?: Function; // No params for type inference
@@ -374,6 +377,12 @@
374
377
  return String(value);
375
378
  };
376
379
 
380
+ const linkHtmlValue = $derived.by(() => {
381
+ if (!linkify) return '';
382
+ const result = convertToHtmlWithLink(getDisplayValue());
383
+ return typeof result === 'string' ? result : String(result ?? '');
384
+ });
385
+
377
386
  const widthStyle = $derived(getStyleFromNumber(width));
378
387
  const maxWidthStyle = $derived(getStyleFromNumber(maxWidth));
379
388
  const minWidthStyle = $derived(getStyleFromNumber(minWidth));
@@ -384,6 +393,7 @@
384
393
  input--focus-{focusStyle}
385
394
  input--type-{type}"
386
395
  class:input--inline={inline}
396
+ class:input--linkify={linkify}
387
397
  class:input--auto-resize={inline}
388
398
  class:input--full-width={fullWidth}
389
399
  class:input--clearable={clearable}
@@ -456,6 +466,11 @@
456
466
  {...restProps}
457
467
  />
458
468
  </div>
469
+ {#if linkify}
470
+ <div class="input__link-text" style={customStyle}>
471
+ {@html linkHtmlValue}
472
+ </div>
473
+ {/if}
459
474
  <!-- クリアボタン -->
460
475
  {#if clearable && !disabled && !readonly}
461
476
  <div class="input__clear-button">
@@ -613,6 +628,35 @@
613
628
  }
614
629
  }
615
630
 
631
+ /* リンク表示用オーバーレイ */
632
+ .input__link-text {
633
+ position: absolute;
634
+ top: 0;
635
+ left: 0;
636
+ width: 100%;
637
+ height: 100%;
638
+ display: flex;
639
+ align-items: center;
640
+ padding: inherit;
641
+ background: transparent;
642
+ border-radius: inherit;
643
+ font-size: inherit;
644
+ font-weight: inherit;
645
+ color: inherit;
646
+ line-height: inherit;
647
+ text-align: inherit;
648
+ white-space: nowrap;
649
+ overflow: hidden;
650
+ text-overflow: ellipsis;
651
+ pointer-events: none;
652
+ z-index: 1;
653
+ }
654
+
655
+ .input__link-text :global(a) {
656
+ pointer-events: auto;
657
+ text-decoration: underline;
658
+ }
659
+
616
660
  .input__clear-button {
617
661
  position: absolute;
618
662
  top: 50%;
@@ -695,7 +739,6 @@
695
739
 
696
740
  input {
697
741
  min-height: var(--svelte-ui-input-height);
698
- padding: var(--svelte-ui-input-padding);
699
742
  background-color: var(--svelte-ui-input-bg);
700
743
  box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-input-border-color);
701
744
  border: none;
@@ -703,30 +746,40 @@
703
746
  color: var(--svelte-ui-input-text-color);
704
747
  }
705
748
 
749
+ input,
750
+ .input__display-text,
751
+ .input__link-text {
752
+ padding: var(--svelte-ui-input-padding);
753
+ }
754
+
706
755
  &.input--has-left-icon {
707
756
  input,
708
- .input__display-text {
757
+ .input__display-text,
758
+ .input__link-text {
709
759
  padding-left: var(--svelte-ui-input-icon-space);
710
760
  }
711
761
  }
712
762
 
713
763
  &.input--has-right-icon {
714
764
  input,
715
- .input__display-text {
765
+ .input__display-text,
766
+ .input__link-text {
716
767
  padding-right: var(--svelte-ui-input-icon-space);
717
768
  }
718
769
  }
719
770
 
720
771
  &.input--clearable {
721
772
  input,
722
- .input__display-text {
773
+ .input__display-text,
774
+ .input__link-text {
723
775
  padding-right: var(--svelte-ui-input-icon-space);
724
776
  }
725
777
  }
726
778
 
727
779
  &.input--clearable.input--has-right-icon {
728
780
  input,
729
- .input__display-text {
781
+ .input__display-text,
782
+ .input__link-text {
730
783
  padding-right: var(--svelte-ui-input-icon-space-double);
731
784
  }
732
785
  }
@@ -741,6 +794,18 @@
741
794
  }
742
795
  }
743
796
 
797
+ /* linkify=true かつフォーカスがないときは、input のテキストカラーだけ透明にして二重描画を防ぐ */
798
+ .input--linkify:not(.input--focused) input {
799
+ color: transparent;
800
+ caret-color: transparent;
801
+ text-shadow: none;
802
+ }
803
+
804
+ /* フォーカス時はリンク用オーバーレイも非表示にして(display:none)、リンクが反応しないようにする */
805
+ .input--focused .input__link-text {
806
+ display: none;
807
+ }
808
+
744
809
  /* =============================================
745
810
  * デザインバリアント:inline
746
811
  * ============================================= */
@@ -759,28 +824,32 @@
759
824
 
760
825
  &.input--has-left-icon {
761
826
  input,
762
- .input__display-text {
827
+ .input__display-text,
828
+ .input__link-text {
763
829
  padding-left: var(--svelte-ui-input-icon-space-inline);
764
830
  }
765
831
  }
766
832
 
767
833
  &.input--has-right-icon {
768
834
  input,
769
- .input__display-text {
835
+ .input__display-text,
836
+ .input__link-text {
770
837
  padding-right: var(--svelte-ui-input-icon-space-inline);
771
838
  }
772
839
  }
773
840
 
774
841
  &.input--clearable {
775
842
  input,
776
- .input__display-text {
843
+ .input__display-text,
844
+ .input__link-text {
777
845
  padding-right: var(--svelte-ui-input-icon-space-inline);
778
846
  }
779
847
  }
780
848
 
781
849
  &.input--clearable.input--has-right-icon {
782
850
  input,
783
- .input__display-text {
851
+ .input__display-text,
852
+ .input__link-text {
784
853
  padding-right: var(--svelte-ui-input-icon-space-double-inline);
785
854
  }
786
855
  }
@@ -796,6 +865,15 @@
796
865
  }
797
866
  }
798
867
 
868
+ /* inline + linkify のときは、display-text を常に隠し、wrapper を常に表示 */
869
+ .input--inline.input--linkify .input__display-text {
870
+ opacity: 0;
871
+ }
872
+
873
+ .input--inline.input--linkify .input__wrapper {
874
+ opacity: 1;
875
+ }
876
+
799
877
  /* =============================================
800
878
  * レイアウトバリエーション
801
879
  * ============================================= */
@@ -812,21 +890,24 @@
812
890
  * ============================================= */
813
891
  .input--clearable {
814
892
  input,
815
- .input__display-text {
893
+ .input__display-text,
894
+ .input__link-text {
816
895
  padding-right: var(--svelte-ui-input-icon-space);
817
896
  }
818
897
  }
819
898
 
820
899
  .input.input--has-right-icon {
821
900
  input,
822
- .input__display-text {
901
+ .input__display-text,
902
+ .input__link-text {
823
903
  padding-right: var(--svelte-ui-input-icon-space);
824
904
  }
825
905
  }
826
906
 
827
907
  .input.input--has-left-icon {
828
908
  input,
829
- .input__display-text {
909
+ .input__display-text,
910
+ .input__link-text {
830
911
  padding-left: var(--svelte-ui-input-icon-space);
831
912
  }
832
913
  }
@@ -37,6 +37,7 @@ type $$ComponentProps = {
37
37
  readonly?: boolean;
38
38
  required?: boolean;
39
39
  clearable?: boolean;
40
+ linkify?: boolean;
40
41
  onfocus?: Function;
41
42
  onblur?: Function;
42
43
  onkeydown?: Function;
@@ -528,6 +528,10 @@
528
528
  color: var(--svelte-ui-text-subtle-color, var(--svelte-ui-text-color));
529
529
  white-space: nowrap;
530
530
  cursor: pointer;
531
+ user-select: none;
532
+ -webkit-user-select: none;
533
+ -moz-user-select: none;
534
+ -ms-user-select: none;
531
535
  transition-property: background-color, color;
532
536
  transition-duration: var(--svelte-ui-transition-duration, 0.2s);
533
537
  transition-timing-function: ease;
@@ -112,7 +112,10 @@
112
112
  );
113
113
  const requiredMargin = -containerHeight;
114
114
 
115
- snackbarRef.style.setProperty('--svelte-ui-snackbar-item-collapse-margin', `${requiredMargin}px`);
115
+ snackbarRef.style.setProperty(
116
+ '--svelte-ui-snackbar-item-collapse-margin',
117
+ `${requiredMargin}px`
118
+ );
116
119
 
117
120
  // CSSカスタムプロパティが確実に設定されるまで少し待つ
118
121
  requestAnimationFrame(() => {
@@ -149,7 +152,8 @@
149
152
  class="snackbar-item__content snackbar-item__content--{type} snackbar-item__content--{variant} snackbar-item__content--{position} {visible
150
153
  ? 'snackbar-item__content--visible'
151
154
  : ''}"
152
- style="--svelte-ui-snackbar-item-custom-color: {color ?? 'unset'}; --svelte-ui-snackbar-item-custom-text-color: {textColor ?? 'unset'};"
155
+ style="--svelte-ui-snackbar-item-custom-color: {color ??
156
+ 'unset'}; --svelte-ui-snackbar-item-custom-text-color: {textColor ?? 'unset'};"
153
157
  role={type === 'error' || type === 'warning' ? 'alert' : 'status'}
154
158
  aria-live={type === 'error' || type === 'warning' ? 'assertive' : 'polite'}
155
159
  aria-atomic="true"
@@ -321,103 +325,208 @@
321
325
 
322
326
  /* Type variants - filled */
323
327
  .snackbar-item__content--filled.snackbar-item__content--info {
324
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-info-filled-bg));
325
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-info-filled-text-color));
328
+ background-color: var(
329
+ --svelte-ui-snackbar-item-custom-color,
330
+ var(--svelte-ui-snackbar-info-filled-bg)
331
+ );
332
+ color: var(
333
+ --svelte-ui-snackbar-item-custom-text-color,
334
+ var(--svelte-ui-snackbar-info-filled-text-color)
335
+ );
326
336
  }
327
337
 
328
338
  .snackbar-item__content--filled.snackbar-item__content--info .snackbar-item__icon {
329
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-info-filled-text-color));
339
+ color: var(
340
+ --svelte-ui-snackbar-item-custom-text-color,
341
+ var(--svelte-ui-snackbar-info-filled-text-color)
342
+ );
330
343
  }
331
344
 
332
345
  .snackbar-item__content--filled.snackbar-item__content--success {
333
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-success-filled-bg));
334
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-success-filled-text-color));
346
+ background-color: var(
347
+ --svelte-ui-snackbar-item-custom-color,
348
+ var(--svelte-ui-snackbar-success-filled-bg)
349
+ );
350
+ color: var(
351
+ --svelte-ui-snackbar-item-custom-text-color,
352
+ var(--svelte-ui-snackbar-success-filled-text-color)
353
+ );
335
354
  }
336
355
 
337
356
  .snackbar-item__content--filled.snackbar-item__content--success .snackbar-item__icon {
338
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-success-filled-text-color));
357
+ color: var(
358
+ --svelte-ui-snackbar-item-custom-text-color,
359
+ var(--svelte-ui-snackbar-success-filled-text-color)
360
+ );
339
361
  }
340
362
 
341
363
  .snackbar-item__content--filled.snackbar-item__content--warning {
342
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-warning-filled-bg));
343
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-warning-filled-text-color));
364
+ background-color: var(
365
+ --svelte-ui-snackbar-item-custom-color,
366
+ var(--svelte-ui-snackbar-warning-filled-bg)
367
+ );
368
+ color: var(
369
+ --svelte-ui-snackbar-item-custom-text-color,
370
+ var(--svelte-ui-snackbar-warning-filled-text-color)
371
+ );
344
372
  }
345
373
 
346
374
  .snackbar-item__content--filled.snackbar-item__content--warning .snackbar-item__icon {
347
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-warning-filled-text-color));
375
+ color: var(
376
+ --svelte-ui-snackbar-item-custom-text-color,
377
+ var(--svelte-ui-snackbar-warning-filled-text-color)
378
+ );
348
379
  }
349
380
 
350
381
  .snackbar-item__content--filled.snackbar-item__content--error {
351
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-error-filled-bg));
352
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-error-filled-text-color));
382
+ background-color: var(
383
+ --svelte-ui-snackbar-item-custom-color,
384
+ var(--svelte-ui-snackbar-error-filled-bg)
385
+ );
386
+ color: var(
387
+ --svelte-ui-snackbar-item-custom-text-color,
388
+ var(--svelte-ui-snackbar-error-filled-text-color)
389
+ );
353
390
  }
354
391
 
355
392
  .snackbar-item__content--filled.snackbar-item__content--error .snackbar-item__icon {
356
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-error-filled-text-color));
393
+ color: var(
394
+ --svelte-ui-snackbar-item-custom-text-color,
395
+ var(--svelte-ui-snackbar-error-filled-text-color)
396
+ );
357
397
  }
358
398
 
359
399
  .snackbar-item__content--filled.snackbar-item__content--default {
360
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-default-filled-bg));
361
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-default-filled-text-color));
400
+ background-color: var(
401
+ --svelte-ui-snackbar-item-custom-color,
402
+ var(--svelte-ui-snackbar-default-filled-bg)
403
+ );
404
+ color: var(
405
+ --svelte-ui-snackbar-item-custom-text-color,
406
+ var(--svelte-ui-snackbar-default-filled-text-color)
407
+ );
362
408
  }
363
409
 
364
410
  .snackbar-item__content--filled.snackbar-item__content--default .snackbar-item__icon {
365
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-default-filled-text-color));
411
+ color: var(
412
+ --svelte-ui-snackbar-item-custom-text-color,
413
+ var(--svelte-ui-snackbar-default-filled-text-color)
414
+ );
366
415
  }
367
416
 
368
417
  /* Type variants - outlined */
369
418
  .snackbar-item__content--outlined.snackbar-item__content--info {
370
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-info-outlined-bg));
371
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-info-outlined-text-color));
419
+ background-color: var(
420
+ --svelte-ui-snackbar-item-custom-color,
421
+ var(--svelte-ui-snackbar-info-outlined-bg)
422
+ );
423
+ color: var(
424
+ --svelte-ui-snackbar-item-custom-text-color,
425
+ var(--svelte-ui-snackbar-info-outlined-text-color)
426
+ );
372
427
  box-shadow: inset 0 0 0 1px
373
- var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-info-outlined-border-color));
428
+ var(
429
+ --svelte-ui-snackbar-item-custom-color,
430
+ var(--svelte-ui-snackbar-info-outlined-border-color)
431
+ );
374
432
  }
375
433
 
376
434
  .snackbar-item__content--outlined.snackbar-item__content--info .snackbar-item__icon {
377
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-info-outlined-text-color));
435
+ color: var(
436
+ --svelte-ui-snackbar-item-custom-text-color,
437
+ var(--svelte-ui-snackbar-info-outlined-text-color)
438
+ );
378
439
  }
379
440
 
380
441
  .snackbar-item__content--outlined.snackbar-item__content--success {
381
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-success-outlined-bg));
382
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-success-outlined-text-color));
442
+ background-color: var(
443
+ --svelte-ui-snackbar-item-custom-color,
444
+ var(--svelte-ui-snackbar-success-outlined-bg)
445
+ );
446
+ color: var(
447
+ --svelte-ui-snackbar-item-custom-text-color,
448
+ var(--svelte-ui-snackbar-success-outlined-text-color)
449
+ );
383
450
  box-shadow: inset 0 0 0 1px
384
- var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-success-outlined-border-color));
451
+ var(
452
+ --svelte-ui-snackbar-item-custom-color,
453
+ var(--svelte-ui-snackbar-success-outlined-border-color)
454
+ );
385
455
  }
386
456
 
387
457
  .snackbar-item__content--outlined.snackbar-item__content--success .snackbar-item__icon {
388
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-success-outlined-text-color));
458
+ color: var(
459
+ --svelte-ui-snackbar-item-custom-text-color,
460
+ var(--svelte-ui-snackbar-success-outlined-text-color)
461
+ );
389
462
  }
390
463
 
391
464
  .snackbar-item__content--outlined.snackbar-item__content--warning {
392
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-warning-outlined-bg));
393
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-warning-outlined-text-color));
465
+ background-color: var(
466
+ --svelte-ui-snackbar-item-custom-color,
467
+ var(--svelte-ui-snackbar-warning-outlined-bg)
468
+ );
469
+ color: var(
470
+ --svelte-ui-snackbar-item-custom-text-color,
471
+ var(--svelte-ui-snackbar-warning-outlined-text-color)
472
+ );
394
473
  box-shadow: inset 0 0 0 1px
395
- var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-warning-outlined-border-color));
474
+ var(
475
+ --svelte-ui-snackbar-item-custom-color,
476
+ var(--svelte-ui-snackbar-warning-outlined-border-color)
477
+ );
396
478
  }
397
479
 
398
480
  .snackbar-item__content--outlined.snackbar-item__content--warning .snackbar-item__icon {
399
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-warning-outlined-text-color));
481
+ color: var(
482
+ --svelte-ui-snackbar-item-custom-text-color,
483
+ var(--svelte-ui-snackbar-warning-outlined-text-color)
484
+ );
400
485
  }
401
486
 
402
487
  .snackbar-item__content--outlined.snackbar-item__content--error {
403
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-error-outlined-bg));
404
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-error-outlined-text-color));
488
+ background-color: var(
489
+ --svelte-ui-snackbar-item-custom-color,
490
+ var(--svelte-ui-snackbar-error-outlined-bg)
491
+ );
492
+ color: var(
493
+ --svelte-ui-snackbar-item-custom-text-color,
494
+ var(--svelte-ui-snackbar-error-outlined-text-color)
495
+ );
405
496
  box-shadow: inset 0 0 0 1px
406
- var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-error-outlined-border-color));
497
+ var(
498
+ --svelte-ui-snackbar-item-custom-color,
499
+ var(--svelte-ui-snackbar-error-outlined-border-color)
500
+ );
407
501
  }
408
502
 
409
503
  .snackbar-item__content--outlined.snackbar-item__content--error .snackbar-item__icon {
410
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-error-outlined-text-color));
504
+ color: var(
505
+ --svelte-ui-snackbar-item-custom-text-color,
506
+ var(--svelte-ui-snackbar-error-outlined-text-color)
507
+ );
411
508
  }
412
509
 
413
510
  .snackbar-item__content--outlined.snackbar-item__content--default {
414
- background-color: var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-default-outlined-bg));
415
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-default-outlined-text-color));
511
+ background-color: var(
512
+ --svelte-ui-snackbar-item-custom-color,
513
+ var(--svelte-ui-snackbar-default-outlined-bg)
514
+ );
515
+ color: var(
516
+ --svelte-ui-snackbar-item-custom-text-color,
517
+ var(--svelte-ui-snackbar-default-outlined-text-color)
518
+ );
416
519
  box-shadow: inset 0 0 0 1px
417
- var(--svelte-ui-snackbar-item-custom-color, var(--svelte-ui-snackbar-default-outlined-border-color));
520
+ var(
521
+ --svelte-ui-snackbar-item-custom-color,
522
+ var(--svelte-ui-snackbar-default-outlined-border-color)
523
+ );
418
524
  }
419
525
 
420
526
  .snackbar-item__content--outlined.snackbar-item__content--default .snackbar-item__icon {
421
- color: var(--svelte-ui-snackbar-item-custom-text-color, var(--svelte-ui-snackbar-default-outlined-text-color));
527
+ color: var(
528
+ --svelte-ui-snackbar-item-custom-text-color,
529
+ var(--svelte-ui-snackbar-default-outlined-text-color)
530
+ );
422
531
  }
423
532
  </style>
@@ -4,6 +4,7 @@
4
4
  import IconButton from './IconButton.svelte';
5
5
  import { getStyleFromNumber } from '../utils/style';
6
6
  import { t } from '../i18n';
7
+ import { convertToHtml, convertToHtmlWithLink } from '../utils/formatText';
7
8
  import type { HTMLTextareaAttributes } from 'svelte/elements';
8
9
  import type { IconVariant } from '../types/icon';
9
10
 
@@ -48,6 +49,7 @@
48
49
  readonly = false,
49
50
  required = false,
50
51
  iconVariant = 'outlined',
52
+ linkify = false,
51
53
 
52
54
  // フォーカスイベント
53
55
  onfocus = () => {}, // No params for type inference
@@ -125,6 +127,7 @@
125
127
  readonly?: boolean;
126
128
  required?: boolean;
127
129
  iconVariant?: IconVariant;
130
+ linkify?: boolean;
128
131
 
129
132
  // フォーカスイベント
130
133
  onfocus?: Function; // No params for type inference
@@ -335,9 +338,7 @@
335
338
  // HTML表示用の値(autoResize時の高さ調整用)
336
339
  const htmlValue = $derived.by(() => {
337
340
  if (value !== '') {
338
- let html = value
339
- .replace(/ +/g, (match) => '&nbsp;'.repeat(match.length))
340
- .replace(/\n/g, '<br />');
341
+ let html = convertToHtml(value) as string;
341
342
  // 最後の行が空だったら空白を追加(高さ調整のため)
342
343
  const lines = html.split('<br />');
343
344
  if (lines.length > 0 && lines[lines.length - 1] === '') {
@@ -348,12 +349,22 @@
348
349
  return '';
349
350
  }
350
351
  });
352
+
353
+ // URLをリンク化した表示用HTML(クリック検出用オーバーレイで使用)
354
+ const linkHtmlValue = $derived.by(() => {
355
+ if (!linkify || value === '') {
356
+ return '';
357
+ }
358
+ const result = convertToHtmlWithLink(value);
359
+ return typeof result === 'string' ? result : String(result ?? '');
360
+ });
351
361
  </script>
352
362
 
353
363
  <div
354
364
  class="textarea
355
365
  textarea--focus-{focusStyle}"
356
366
  class:textarea--inline={inline}
367
+ class:textarea--linkify={linkify}
357
368
  class:textarea--full-width={fullWidth}
358
369
  class:textarea--full-height={fullHeight}
359
370
  class:textarea--auto-resize={autoResize}
@@ -438,6 +449,11 @@
438
449
  </div>
439
450
  {/if}
440
451
  </div>
452
+ {#if linkify}
453
+ <div class="textarea__link-text" style="{minHeightStyle} {customStyle}">
454
+ {@html linkHtmlValue}
455
+ </div>
456
+ {/if}
441
457
  </div>
442
458
 
443
459
  <style>
@@ -471,11 +487,11 @@
471
487
  }
472
488
 
473
489
  /* =============================================
474
- * 基本コンポーネント
475
- * ============================================= */
476
- .textarea__display-text {
477
- display: flex;
478
- align-items: start; /* テーブルの他の列に合わせて高さが高くなっているときに、上寄せになるようにするための措置 */
490
+ * 基本コンポーネント
491
+ * ============================================= */
492
+ .textarea__display-text,
493
+ .textarea__link-text {
494
+ display: block;
479
495
  width: 100%;
480
496
  background: inherit;
481
497
  border: inherit;
@@ -498,6 +514,23 @@
498
514
  }
499
515
  }
500
516
 
517
+ /* クリック可能なリンク用オーバーレイ */
518
+ .textarea__link-text {
519
+ position: absolute;
520
+ top: 0;
521
+ left: 0;
522
+ width: 100%;
523
+ height: 100%;
524
+ padding: inherit;
525
+ pointer-events: none;
526
+ z-index: 1;
527
+ }
528
+
529
+ .textarea__link-text :global(a) {
530
+ pointer-events: auto;
531
+ text-decoration: underline;
532
+ }
533
+
501
534
  textarea {
502
535
  position: absolute;
503
536
  top: 0;
@@ -626,8 +659,8 @@
626
659
  }
627
660
 
628
661
  /* =============================================
629
- * 表示切り替え(フォーカス時・非autoResize時)
630
- * ============================================= */
662
+ * 表示切り替え(フォーカス時・非inline)
663
+ * ============================================= */
631
664
  .textarea--focused,
632
665
  .textarea:not(.textarea--inline) {
633
666
  .textarea__display-text {
@@ -639,11 +672,22 @@
639
672
  }
640
673
  }
641
674
 
675
+ /* linkify=true かつ非 inline のときは、display-text は常に非表示(レイアウトだけ保持) */
676
+ .textarea--linkify:not(.textarea--inline) .textarea__display-text {
677
+ opacity: 0;
678
+ }
679
+
680
+ /* フォーカス時はリンク用オーバーレイも非表示にして(display:none)、リンクが反応しないようにする */
681
+ .textarea--focused .textarea__link-text {
682
+ display: none;
683
+ }
684
+
642
685
  /* =============================================
643
686
  * デザインバリアント:default
644
687
  * ============================================= */
645
688
  .textarea:not(.textarea--inline) {
646
- .textarea__display-text {
689
+ .textarea__display-text,
690
+ .textarea__link-text {
647
691
  padding: var(--svelte-ui-textarea-padding);
648
692
  }
649
693
 
@@ -664,6 +708,15 @@
664
708
  }
665
709
  }
666
710
 
711
+ /* linkify=true かつフォーカスがないときは、textarea のテキストカラーだけ透明にして二重描画を防ぐ
712
+ * placeholder の色は textarea::placeholder 側で指定しているため、この指定の影響を受けない
713
+ */
714
+ .textarea--linkify:not(.textarea--focused) textarea {
715
+ color: transparent;
716
+ caret-color: transparent;
717
+ text-shadow: none;
718
+ }
719
+
667
720
  /* =============================================
668
721
  * デザインバリアント:rounded
669
722
  * ============================================= */
@@ -699,4 +752,13 @@
699
752
  top: var(--svelte-ui-textarea-icon-top-inline);
700
753
  }
701
754
  }
755
+
756
+ /* inline + linkify のときは、display-text を常に隠し、textarea を常に表示 */
757
+ .textarea--inline.textarea--linkify .textarea__display-text {
758
+ opacity: 0;
759
+ }
760
+
761
+ .textarea--inline.textarea--linkify textarea {
762
+ opacity: 1;
763
+ }
702
764
  </style>
@@ -30,6 +30,7 @@ type $$ComponentProps = {
30
30
  readonly?: boolean;
31
31
  required?: boolean;
32
32
  iconVariant?: IconVariant;
33
+ linkify?: boolean;
33
34
  onfocus?: Function;
34
35
  onblur?: Function;
35
36
  onkeydown?: Function;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@14ch/svelte-ui",
3
3
  "description": "Modern Svelte UI components library with TypeScript support",
4
4
  "private": false,
5
- "version": "0.0.7",
5
+ "version": "0.0.9",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",