@14ch/svelte-ui 0.0.8 → 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.
@@ -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
  };
@@ -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;
@@ -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.8",
5
+ "version": "0.0.9",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",