@14ch/svelte-ui 0.0.14 → 0.0.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.
@@ -376,7 +376,7 @@
376
376
  --svelte-ui-input-icon-space-double-inline: calc(var(--svelte-ui-form-icon-space) * 2);
377
377
  --svelte-ui-input-bg: var(--svelte-ui-form-bg);
378
378
  --svelte-ui-input-border-color: var(--svelte-ui-border-weak-color);
379
- --svelte-ui-input-text-color: var(--svelte-ui-text-color);
379
+ --svelte-ui-input-unit-font-size: max(0.5em, var(--svelte-ui-font-size-sm));
380
380
  --svelte-ui-input-placeholder-color: var(--svelte-ui-text-placeholder-color);
381
381
  --svelte-ui-input-icon-color: var(--svelte-ui-text-subtle-color);
382
382
 
@@ -387,7 +387,6 @@
387
387
  --svelte-ui-textarea-border-radius-rounded: calc(var(--svelte-ui-textarea-min-height) / 2);
388
388
  --svelte-ui-textarea-bg: var(--svelte-ui-form-bg);
389
389
  --svelte-ui-textarea-border-color: var(--svelte-ui-border-weak-color);
390
- --svelte-ui-textarea-text-color: var(--svelte-ui-text-color);
391
390
  --svelte-ui-textarea-placeholder-color: var(--svelte-ui-text-placeholder-color);
392
391
  --svelte-ui-textarea-icon-space: calc(
393
392
  var(--svelte-ui-form-icon-space) + var(--svelte-ui-form-icon-offset)
@@ -409,7 +408,6 @@
409
408
  --svelte-ui-select-border-radius-rounded: var(--svelte-ui-form-border-radius-rounded);
410
409
  --svelte-ui-select-bg: var(--svelte-ui-form-bg);
411
410
  --svelte-ui-select-border-color: var(--svelte-ui-border-weak-color);
412
- --svelte-ui-select-text-color: var(--svelte-ui-text-color);
413
411
  --svelte-ui-select-dropdown-icon-color: var(--svelte-ui-dropdown-icon-color);
414
412
  --svelte-ui-select-placeholder-color: var(--svelte-ui-text-placeholder-color);
415
413
 
@@ -510,7 +508,6 @@
510
508
  --svelte-ui-colorpicker-border-color: var(--svelte-ui-border-weak-color);
511
509
  --svelte-ui-colorpicker-trigger-border-color: var(--svelte-ui-border-color);
512
510
  --svelte-ui-colorpicker-bg: var(--svelte-ui-form-bg);
513
- --svelte-ui-colorpicker-text-color: var(--svelte-ui-text-color);
514
511
  --svelte-ui-colorpicker-placeholder-color: var(--svelte-ui-text-placeholder-color);
515
512
 
516
513
  /* Combobox */
@@ -524,7 +521,6 @@
524
521
  --svelte-ui-combobox-border-radius-rounded: var(--svelte-ui-form-border-radius-rounded);
525
522
  --svelte-ui-combobox-border-color: var(--svelte-ui-border-weak-color);
526
523
  --svelte-ui-combobox-bg: var(--svelte-ui-surface-color);
527
- --svelte-ui-combobox-text-color: var(--svelte-ui-text-color);
528
524
  --svelte-ui-combobox-dropdown-icon-color: var(--svelte-ui-dropdown-icon-color);
529
525
  --svelte-ui-combobox-option-hover-bg: var(--svelte-ui-hover-overlay);
530
526
  --svelte-ui-combobox-option-selected-bg: var(--svelte-ui-select-overlay);
@@ -339,10 +339,11 @@
339
339
  * ========================================================================= */
340
340
 
341
341
  .checkbox {
342
- display: flex;
342
+ display: inline-flex;
343
343
  align-items: center;
344
344
  width: fit-content;
345
345
  min-height: var(--svelte-ui-checkbox-min-height);
346
+ vertical-align: top;
346
347
  contain: layout;
347
348
  }
348
349
 
@@ -19,7 +19,7 @@
19
19
  // =========================================================================
20
20
  export type ColorPickerProps = {
21
21
  // 基本プロパティ
22
- value: string;
22
+ value: string | null | undefined;
23
23
 
24
24
  // HTML属性系
25
25
  id?: string;
@@ -141,7 +141,12 @@
141
141
  ...restProps
142
142
  }: ColorPickerProps = $props();
143
143
 
144
- let localValue: string | undefined = $state(value);
144
+ const getNormalizedValue = (val: string | null | undefined): string => {
145
+ if (val === null || val === undefined) return '';
146
+ return val;
147
+ };
148
+
149
+ let localValue: string = $state(getNormalizedValue(value));
145
150
  let prevValue: string = $state('');
146
151
  let isFocused: boolean = $state(false);
147
152
 
@@ -154,7 +159,7 @@
154
159
  value;
155
160
  untrack(() => {
156
161
  /* value を localValue に反映 */
157
- localValue = value;
162
+ localValue = getNormalizedValue(value);
158
163
 
159
164
  /* value が更新されたらonchangeを実行 */
160
165
  handleValueChange();
@@ -165,7 +170,7 @@
165
170
  localValue;
166
171
  untrack(() => {
167
172
  /* localValue がクリアされた時に value もクリア */
168
- if (localValue === '' || localValue === undefined) {
173
+ if (localValue === '') {
169
174
  value = '';
170
175
  prevValue = '';
171
176
  }
@@ -392,7 +397,10 @@
392
397
  {...inputAttributes}
393
398
  {...restProps}
394
399
  />
395
- <div class="color-picker__color-display" style="background-color: {value};"></div>
400
+ <div
401
+ class="color-picker__color-display"
402
+ style="background-color: {getNormalizedValue(value)};"
403
+ ></div>
396
404
  </div>
397
405
  </div>
398
406
 
@@ -407,6 +415,7 @@
407
415
  position: relative;
408
416
  width: auto;
409
417
  max-width: 100%;
418
+ vertical-align: top;
410
419
  }
411
420
 
412
421
  /* =============================================
@@ -2,7 +2,7 @@ import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  import type { IconVariant } from '../types/icon';
3
3
  import type { FocusHandler, KeyboardHandler, MouseHandler, TouchHandler, PointerHandler } from '../types/callbackHandlers';
4
4
  export type ColorPickerProps = {
5
- value: string;
5
+ value: string | null | undefined;
6
6
  id?: string;
7
7
  inputAttributes?: HTMLInputAttributes | undefined;
8
8
  customStyle?: string;
@@ -518,6 +518,7 @@
518
518
  position: relative;
519
519
  width: auto;
520
520
  max-width: 100%;
521
+ vertical-align: top;
521
522
  }
522
523
 
523
524
  .combobox--full-width {
@@ -547,7 +548,7 @@
547
548
  text-align: left;
548
549
  cursor: pointer;
549
550
  font-size: inherit;
550
- color: var(--svelte-ui-combobox-text-color);
551
+ color: var(--svelte-ui-text-color);
551
552
  transition: background-color var(--svelte-ui-transition-duration) ease;
552
553
 
553
554
  @media (hover: hover) {
@@ -234,13 +234,13 @@
234
234
  // =========================================================================
235
235
  // Effects
236
236
  // =========================================================================
237
- const effectiveLocale = $derived(locale ?? getLocale());
237
+ const resolvedLocale = $derived(locale ?? getLocale());
238
238
 
239
239
  $effect(() => {
240
240
  // range モードのときは、value を「start <= end」の順序に正規化
241
241
  if (mode === 'range' && value && 'start' in value && 'end' in value) {
242
- const startDay = dayjs(value.start).locale(effectiveLocale);
243
- const endDay = dayjs(value.end).locale(effectiveLocale);
242
+ const startDay = dayjs(value.start).locale(resolvedLocale);
243
+ const endDay = dayjs(value.end).locale(resolvedLocale);
244
244
  if (startDay.isAfter(endDay)) {
245
245
  value = { start: value.end, end: value.start };
246
246
  }
@@ -248,8 +248,7 @@
248
248
  });
249
249
 
250
250
  $effect(() => {
251
- const formatWithLocale = (date: Date) =>
252
- dayjs(date).locale(effectiveLocale).format(finalFormat);
251
+ const formatWithLocale = (date: Date) => dayjs(date).locale(resolvedLocale).format(finalFormat);
253
252
 
254
253
  if (mode === 'range' && value && 'start' in value && 'end' in value) {
255
254
  displayValue = `${formatWithLocale(value.start)}${rangeSeparator}${formatWithLocale(value.end)}`;
@@ -267,11 +266,11 @@
267
266
  // スクリーンリーダーアナウンス
268
267
  if (value) {
269
268
  if (mode === 'range' && typeof value === 'object' && 'start' in value && 'end' in value) {
270
- const startDate = dayjs(value.start).locale(effectiveLocale).format(finalFormat);
271
- const endDate = dayjs(value.end).locale(effectiveLocale).format(finalFormat);
269
+ const startDate = dayjs(value.start).locale(resolvedLocale).format(finalFormat);
270
+ const endDate = dayjs(value.end).locale(resolvedLocale).format(finalFormat);
272
271
  announceToScreenReader(`Date range selected: ${startDate} to ${endDate}`);
273
272
  } else if (value instanceof Date) {
274
- const formattedDate = dayjs(value).locale(effectiveLocale).format(finalFormat);
273
+ const formattedDate = dayjs(value).locale(resolvedLocale).format(finalFormat);
275
274
  announceToScreenReader(`Date selected: ${formattedDate}`);
276
275
  }
277
276
  }
@@ -512,7 +511,7 @@
512
511
 
513
512
  // 日付パース処理
514
513
  if (mode === 'range') {
515
- const parsedRange = parseRangeInput(inputStr, effectiveLocale);
514
+ const parsedRange = parseRangeInput(inputStr, resolvedLocale);
516
515
  if (!parsedRange) return;
517
516
 
518
517
  value = parsedRange;
@@ -521,7 +520,7 @@
521
520
  }
522
521
 
523
522
  // single モードでは先頭の「日付本体」のみを解釈する
524
- const parsedSingle = parseSingleInput(inputStr, effectiveLocale);
523
+ const parsedSingle = parseSingleInput(inputStr, resolvedLocale);
525
524
  if (!parsedSingle) return;
526
525
 
527
526
  value = parsedSingle;
@@ -589,7 +588,7 @@
589
588
  // $derived
590
589
  // =========================================================================
591
590
  const calendarId = $derived(`${id}-calendar`);
592
- const currentLocaleConfig = $derived(localeConfig[effectiveLocale]);
591
+ const currentLocaleConfig = $derived(localeConfig[resolvedLocale]);
593
592
  const finalFormat = $derived(
594
593
  format ||
595
594
  (mode === 'range' ? currentLocaleConfig.rangeFormat : currentLocaleConfig.defaultFormat)
@@ -670,7 +669,7 @@
670
669
  onchange={handleChange}
671
670
  {minDate}
672
671
  {maxDate}
673
- locale={effectiveLocale}
672
+ locale={resolvedLocale}
674
673
  id={calendarId}
675
674
  />
676
675
  </Popup>
@@ -679,6 +678,7 @@
679
678
  position: relative;
680
679
  display: inline-block;
681
680
  width: auto;
681
+ vertical-align: top;
682
682
  }
683
683
  .datepicker.datepicker--full-width {
684
684
  display: block;
@@ -34,7 +34,7 @@
34
34
  id?: string;
35
35
 
36
36
  // スタイル/レイアウト
37
- width?: string | number;
37
+ width?: string | number | undefined;
38
38
  position?: 'left' | 'right';
39
39
  bodyStyle?: string;
40
40
  noPadding?: boolean;
@@ -64,7 +64,7 @@
64
64
  id,
65
65
 
66
66
  // スタイル/レイアウト
67
- width = 240,
67
+ width = undefined,
68
68
  position = 'left',
69
69
  bodyStyle = '',
70
70
  noPadding = false,
@@ -106,7 +106,11 @@
106
106
  // =========================================================================
107
107
  const drawerStyles = $derived(() => {
108
108
  const styles = [];
109
- styles.push(`width: ${getStyleFromNumber(width)}`);
109
+ if (width !== undefined) {
110
+ styles.push(`width: ${getStyleFromNumber(width)}`);
111
+ } else {
112
+ styles.push('width: max-content');
113
+ }
110
114
  styles.push('height: 100%');
111
115
  styles.push('min-height: 100%');
112
116
  styles.push(`${position}: 0`);
@@ -18,7 +18,7 @@ export type DrawerProps = {
18
18
  title?: string;
19
19
  description?: string;
20
20
  id?: string;
21
- width?: string | number;
21
+ width?: string | number | undefined;
22
22
  position?: 'left' | 'right';
23
23
  bodyStyle?: string;
24
24
  noPadding?: boolean;
@@ -17,7 +17,7 @@
17
17
  fallbackText?: string;
18
18
 
19
19
  // スタイル/レイアウト
20
- size?: number;
20
+ size?: number | string;
21
21
  color?: string;
22
22
  customStyle?: string;
23
23
 
@@ -53,7 +53,7 @@
53
53
  filled = false,
54
54
  weight = 300,
55
55
  grade = 0,
56
- opticalSize = size,
56
+ opticalSize = typeof size === 'number' ? size : 24,
57
57
  variant = 'outlined',
58
58
 
59
59
  // ARIA/アクセシビリティ
@@ -61,13 +61,16 @@
61
61
  decorative = true,
62
62
 
63
63
  // その他
64
+ class: className,
64
65
  ...restProps
65
66
  }: IconProps = $props();
66
67
 
67
68
  // =========================================================================
68
69
  // $derived
69
70
  // =========================================================================
70
- const iconClasses = $derived(`material-symbols-${variant}`);
71
+ const iconClasses = $derived(
72
+ ['icon', `icon--${variant}`, `material-symbols-${variant}`, className].filter(Boolean).join(' ')
73
+ );
71
74
 
72
75
  const fontVariationSettings = $derived(
73
76
  `'FILL' ${filled ? 1 : 0}, 'wght' ${weight}, 'GRAD' ${grade}, 'opsz' ${opticalSize}`
@@ -79,12 +82,13 @@
79
82
  role: !decorative && ariaLabel ? 'img' : undefined
80
83
  });
81
84
 
82
- const iconStyle = $derived(
83
- `width: ${size}px; height: ${size}px; font-size: ${size}px;
84
- color: ${color}; line-height: 1;
85
+ const iconStyle = $derived.by(() => {
86
+ const sizeStyle = getStyleFromNumber(size);
87
+ return `width: ${sizeStyle}; height: ${sizeStyle}; font-size: ${sizeStyle};
88
+ color: ${color};
85
89
  font-variation-settings: ${fontVariationSettings};
86
- ${customStyle}`
87
- );
90
+ ${customStyle}`;
91
+ });
88
92
  </script>
89
93
 
90
94
  <i
@@ -102,7 +106,7 @@
102
106
  <!-- Unicode文字での代替表示 -->
103
107
  <span
104
108
  class="icon-fallback-text"
105
- style="width: {size}px; height: {size}px; font-size: {size}px; {customStyle}"
109
+ style={customStyle}
106
110
  {...ariaAttributes}
107
111
  {...restProps}
108
112
  data-testid="icon-fallback"
@@ -112,13 +116,11 @@
112
116
  {/if}
113
117
 
114
118
  <style>
115
- .material-symbols-outlined,
116
- .material-symbols-rounded,
117
- .material-symbols-sharp {
119
+ .icon {
118
120
  display: block;
119
121
  font-size: inherit;
120
122
  color: inherit;
121
- line-height: inherit;
123
+ line-height: 1;
122
124
  text-transform: none;
123
125
  letter-spacing: normal;
124
126
  word-wrap: normal;
@@ -129,15 +131,15 @@
129
131
  transition-timing-function: ease;
130
132
  }
131
133
 
132
- .material-symbols-outlined {
134
+ .icon.material-symbols-outlined {
133
135
  font-family: 'Material Symbols Outlined';
134
136
  }
135
137
 
136
- .material-symbols-rounded {
138
+ .icon.material-symbols-rounded {
137
139
  font-family: 'Material Symbols Rounded';
138
140
  }
139
141
 
140
- .material-symbols-sharp {
142
+ .icon.material-symbols-sharp {
141
143
  font-family: 'Material Symbols Sharp';
142
144
  }
143
145
 
@@ -157,10 +159,7 @@
157
159
 
158
160
  /* Prefers reduced motion */
159
161
  @media (prefers-reduced-motion: reduce) {
160
- .material-symbols-outlined,
161
- .material-symbols-filled,
162
- .material-symbols-rounded,
163
- .material-symbols-sharp,
162
+ .icon,
164
163
  .icon-fallback-text {
165
164
  transition: none;
166
165
  }
@@ -168,10 +167,7 @@
168
167
 
169
168
  /* Print styles */
170
169
  @media print {
171
- .material-symbols-outlined,
172
- .material-symbols-filled,
173
- .material-symbols-rounded,
174
- .material-symbols-sharp,
170
+ .icon,
175
171
  .icon-fallback-text {
176
172
  color: black !important;
177
173
  }
@@ -4,7 +4,7 @@ export type IconProps = {
4
4
  children: Snippet;
5
5
  title?: string;
6
6
  fallbackText?: string;
7
- size?: number;
7
+ size?: number | string;
8
8
  color?: string;
9
9
  customStyle?: string;
10
10
  filled?: boolean;
@@ -50,6 +50,7 @@
50
50
  maxWidth?: string | number | null;
51
51
  rounded?: boolean;
52
52
  customStyle?: string;
53
+ unit?: string;
53
54
 
54
55
  // アイコン関連
55
56
  rightIcon?: string;
@@ -137,12 +138,13 @@
137
138
  // スタイル/レイアウト
138
139
  inline = false,
139
140
  focusStyle = 'outline',
140
- customStyle = '',
141
141
  fullWidth = false,
142
142
  width = null,
143
143
  minWidth = inline ? null : 120,
144
144
  maxWidth = null,
145
145
  rounded = false,
146
+ customStyle = '',
147
+ unit = '',
146
148
 
147
149
  // アイコン関連
148
150
  rightIcon = undefined,
@@ -381,17 +383,24 @@
381
383
  // =========================================================================
382
384
  // $derived
383
385
  // =========================================================================
384
- const getDisplayValue = (): string => {
385
- if (value === null || value === undefined) return '';
386
+ const displayValue = $derived.by(() => {
387
+ if (!value) {
388
+ if (inline && !placeholder) {
389
+ return '&nbsp;';
390
+ }
391
+ return '';
392
+ }
386
393
  if (type === 'number' && typeof value === 'number') {
387
394
  return value.toLocaleString();
388
395
  }
389
- return String(value);
390
- };
396
+ return convertToHtmlWithLink(value);
397
+ });
398
+
399
+ const isLinkifyActive = $derived(linkify && (type === 'text' || type === 'url'));
391
400
 
392
401
  const linkHtmlValue = $derived.by(() => {
393
- if (!linkify) return '';
394
- const result = convertToHtmlWithLink(getDisplayValue());
402
+ if (!isLinkifyActive) return '';
403
+ const result = convertToHtmlWithLink(value);
395
404
  return typeof result === 'string' ? result : String(result ?? '');
396
405
  });
397
406
 
@@ -405,7 +414,7 @@
405
414
  input--focus-{focusStyle}
406
415
  input--type-{type}"
407
416
  class:input--inline={inline}
408
- class:input--linkify={linkify}
417
+ class:input--linkify={isLinkifyActive}
409
418
  class:input--auto-resize={inline}
410
419
  class:input--full-width={fullWidth}
411
420
  class:input--clearable={clearable}
@@ -419,12 +428,17 @@
419
428
  data-testid="input"
420
429
  style="width: {widthStyle}; max-width: {maxWidthStyle}; min-width: {minWidthStyle}"
421
430
  >
422
- <!-- inline時の表示用要素(text-overflow: ellipsisが効く) -->
423
- {#if inline}
424
- <div class="input__display-text" data-placeholder={placeholder} style={customStyle}>
425
- {getDisplayValue()}
431
+ <!-- 表示用テキスト -->
432
+ <div class="input__display-text" data-placeholder={placeholder} style={customStyle}>
433
+ <div class="input__display-text-content">
434
+ {@html displayValue}
435
+ {#if type === 'number' && unit !== ''}
436
+ <span class="input__unit-text">
437
+ {unit}
438
+ </span>
439
+ {/if}
426
440
  </div>
427
- {/if}
441
+ </div>
428
442
  <!-- 入力用要素 -->
429
443
  <div class="input__wrapper">
430
444
  <input
@@ -478,9 +492,11 @@
478
492
  {...restProps}
479
493
  />
480
494
  </div>
481
- {#if linkify}
495
+ {#if isLinkifyActive}
482
496
  <div class="input__link-text" style={customStyle}>
483
- {@html linkHtmlValue}
497
+ <div class="input__link-text-content">
498
+ {@html linkHtmlValue}
499
+ </div>
484
500
  </div>
485
501
  {/if}
486
502
  <!-- クリアボタン -->
@@ -488,7 +504,7 @@
488
504
  <div class="input__clear-button">
489
505
  <IconButton
490
506
  ariaLabel={t('input.clear')}
491
- color="var(--svelte-ui-input-text-color)"
507
+ color="var(--svelte-ui-text-color)"
492
508
  onclick={(event) => {
493
509
  event.stopPropagation();
494
510
  clear();
@@ -568,12 +584,19 @@
568
584
  .input {
569
585
  display: inline-block;
570
586
  position: relative;
587
+ vertical-align: top;
571
588
  width: auto;
589
+ height: var(--svelte-ui-input-height);
572
590
  max-width: 100%;
573
591
  height: inherit;
574
592
  }
575
593
 
576
594
  .input__wrapper {
595
+ position: absolute;
596
+ top: 0;
597
+ left: 0;
598
+ width: 100%;
599
+ height: 100%;
577
600
  padding: inherit;
578
601
  border: none;
579
602
  font-size: inherit;
@@ -581,7 +604,7 @@
581
604
  color: inherit;
582
605
  line-height: inherit;
583
606
  text-align: inherit;
584
- opacity: 0;
607
+ opacity: 1;
585
608
  }
586
609
 
587
610
  /* =============================================
@@ -610,63 +633,40 @@
610
633
  }
611
634
  }
612
635
 
613
- .input__display-text {
614
- display: inline-block;
615
- vertical-align: top;
636
+ .input__display-text,
637
+ .input__link-text {
638
+ display: flex;
639
+ align-items: center;
616
640
  width: 100%;
617
641
  min-width: 1em;
618
642
  padding: inherit;
619
- background: inherit;
620
- border: inherit;
621
643
  font-size: inherit;
622
644
  font-weight: inherit;
623
645
  color: inherit;
624
646
  line-height: inherit;
625
647
  text-align: inherit;
626
648
  white-space: nowrap;
627
- vertical-align: top;
628
649
  overflow: hidden;
629
650
  text-overflow: ellipsis;
630
651
  opacity: 1;
631
652
  transition: none;
632
- cursor: text !important;
633
-
634
- &::before {
635
- content: '';
636
- }
637
-
638
- &:empty::before {
639
- content: attr(data-placeholder);
640
- }
641
653
  }
642
654
 
643
- /* リンク表示用オーバーレイ */
644
655
  .input__link-text {
645
656
  position: absolute;
646
657
  top: 0;
647
658
  left: 0;
648
- width: 100%;
649
659
  height: 100%;
650
- display: flex;
651
- align-items: center;
652
- padding: inherit;
653
- background: transparent;
654
- border-radius: inherit;
655
- font-size: inherit;
656
- font-weight: inherit;
657
- color: inherit;
658
- line-height: inherit;
659
- text-align: inherit;
660
- white-space: nowrap;
661
- overflow: hidden;
662
- text-overflow: ellipsis;
663
660
  pointer-events: none;
664
661
  z-index: 1;
665
662
  }
666
663
 
667
664
  .input__link-text :global(a) {
668
665
  pointer-events: auto;
669
- text-decoration: underline;
666
+ }
667
+
668
+ .input__unit-text {
669
+ font-size: var(--svelte-ui-input-unit-font-size);
670
670
  }
671
671
 
672
672
  .input__clear-button {
@@ -741,26 +741,45 @@
741
741
  }
742
742
 
743
743
  /* =============================================
744
- * デザインバリアント:default
744
+ * タイプ別スタイル
745
745
  * ============================================= */
746
- .input:not(.input--inline) {
747
- .input__wrapper {
748
- position: static;
749
- opacity: 1;
746
+ /* type-number */
747
+ .input--type-number {
748
+ .input__display-text,
749
+ .input__link-text {
750
+ justify-content: flex-end;
750
751
  }
752
+ }
753
+
754
+ /* type-password */
755
+ .input--type-password {
756
+ &:not(.input--linkify) input {
757
+ color: inherit;
758
+ caret-color: inherit;
759
+ text-shadow: inherit;
760
+ }
761
+
762
+ .input__display-text {
763
+ opacity: 0;
764
+ }
765
+ }
751
766
 
767
+ /* =============================================
768
+ * デザインバリアント:default
769
+ * ============================================= */
770
+ .input:not(.input--inline) {
752
771
  input {
753
772
  min-height: var(--svelte-ui-input-height);
754
773
  background-color: var(--svelte-ui-input-bg);
755
774
  box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-input-border-color);
756
775
  border: none;
757
776
  border-radius: var(--svelte-ui-input-border-radius);
758
- color: var(--svelte-ui-input-text-color);
759
777
  }
760
778
 
761
779
  input,
762
780
  .input__display-text,
763
781
  .input__link-text {
782
+ height: var(--svelte-ui-input-height);
764
783
  padding: var(--svelte-ui-input-padding);
765
784
  }
766
785
 
@@ -806,34 +825,10 @@
806
825
  }
807
826
  }
808
827
 
809
- /* linkify=true かつフォーカスがないときは、input のテキストカラーだけ透明にして二重描画を防ぐ */
810
- .input--linkify:not(.input--focused) input {
811
- color: transparent;
812
- caret-color: transparent;
813
- text-shadow: none;
814
- }
815
-
816
- /* フォーカス時はリンク用オーバーレイも非表示にして(display:none)、リンクが反応しないようにする */
817
- .input--focused .input__link-text {
818
- display: none;
819
- }
820
-
821
828
  /* =============================================
822
829
  * デザインバリアント:inline
823
830
  * ============================================= */
824
831
  .input--inline {
825
- &.input--type-number .input__display-text {
826
- text-align: right;
827
- }
828
-
829
- .input__wrapper {
830
- position: absolute;
831
- top: 0;
832
- left: 0;
833
- width: 100%;
834
- height: 100%;
835
- }
836
-
837
832
  &.input--has-left-icon {
838
833
  input,
839
834
  .input__display-text,
@@ -865,25 +860,6 @@
865
860
  padding-right: var(--svelte-ui-input-icon-space-double-inline);
866
861
  }
867
862
  }
868
-
869
- &.input--focused {
870
- .input__display-text {
871
- opacity: 0;
872
- }
873
-
874
- .input__wrapper {
875
- opacity: 1;
876
- }
877
- }
878
- }
879
-
880
- /* inline + linkify のときは、display-text を常に隠し、wrapper を常に表示 */
881
- .input--inline.input--linkify .input__display-text {
882
- opacity: 0;
883
- }
884
-
885
- .input--inline.input--linkify .input__wrapper {
886
- opacity: 1;
887
863
  }
888
864
 
889
865
  /* =============================================
@@ -924,6 +900,39 @@
924
900
  }
925
901
  }
926
902
 
903
+ /* not-linkify + not-focused: inputを不可視化、display-textを表示 */
904
+ /* type=password のときは除外(常に input を表示) */
905
+ .input:not(.input--linkify):not(.input--focused):not(.input--type-password) input {
906
+ color: transparent;
907
+ caret-color: transparent;
908
+ text-shadow: none;
909
+ }
910
+
911
+ /* not-linkify + focused: display-textをopacity: 0 */
912
+ .input:not(.input--linkify).input--focused .input__display-text {
913
+ opacity: 0;
914
+ }
915
+
916
+ /* linkify + not-focused: inputを不可視化、display-textをopacity: 0、link-textを表示 */
917
+ .input--linkify:not(.input--focused) input {
918
+ color: transparent;
919
+ caret-color: transparent;
920
+ text-shadow: none;
921
+ }
922
+
923
+ .input--linkify:not(.input--focused) .input__display-text {
924
+ opacity: 0;
925
+ }
926
+
927
+ /* linkify + focused: display-textをopacity: 0、link-textをdisplay: none */
928
+ .input--linkify.input--focused .input__display-text {
929
+ opacity: 0;
930
+ }
931
+
932
+ .input--focused .input__link-text {
933
+ display: none;
934
+ }
935
+
927
936
  /* =============================================
928
937
  * プレースホルダー・テキスト表示
929
938
  * ============================================= */
@@ -26,6 +26,7 @@ export type InputProps = {
26
26
  maxWidth?: string | number | null;
27
27
  rounded?: boolean;
28
28
  customStyle?: string;
29
+ unit?: string;
29
30
  rightIcon?: string;
30
31
  leftIcon?: string;
31
32
  leftIconAriaLabel?: string;
@@ -365,10 +365,11 @@
365
365
  * ============================================= */
366
366
 
367
367
  .radio {
368
- display: flex;
368
+ display: inline-flex;
369
369
  align-items: center;
370
370
  width: fit-content;
371
371
  min-height: var(--svelte-ui-radio-min-height);
372
+ vertical-align: top;
372
373
  contain: layout;
373
374
  }
374
375
 
@@ -376,7 +376,7 @@
376
376
  // =========================================================================
377
377
  // $derived
378
378
  // =========================================================================
379
- const effectiveIconSize = $derived(
379
+ const resolvedIconSize = $derived(
380
380
  iconOpticalSize || (size === 'small' ? 16 : size === 'large' ? 24 : 20)
381
381
  );
382
382
 
@@ -458,7 +458,7 @@
458
458
  filled={iconFilled || isSelected}
459
459
  weight={iconWeight}
460
460
  grade={iconGrade}
461
- opticalSize={effectiveIconSize}
461
+ opticalSize={resolvedIconSize}
462
462
  variant={iconVariant}
463
463
  >
464
464
  {item.icon}
@@ -474,7 +474,7 @@
474
474
  </div>
475
475
 
476
476
  <style>.segmented-control {
477
- display: inline-flex;
477
+ display: flex;
478
478
  position: relative;
479
479
  padding: var(--svelte-ui-segmented-control-base-padding);
480
480
  background-color: var(--svelte-ui-segmented-control-base-bg);
@@ -355,6 +355,7 @@ select--focus-{focusStyle}"
355
355
  position: relative;
356
356
  width: auto;
357
357
  max-width: 100%;
358
+ vertical-align: top;
358
359
  }
359
360
 
360
361
  /* =============================================
@@ -453,7 +454,6 @@ select--focus-{focusStyle}"
453
454
  border: none;
454
455
  border-radius: var(--svelte-ui-select-border-radius);
455
456
  font-size: 1rem;
456
- color: var(--svelte-ui-select-text-color);
457
457
  line-height: var(--svelte-ui-select-height);
458
458
  }
459
459
  }
@@ -171,6 +171,7 @@
171
171
  {#each tabItems as tabItem, index}
172
172
  <TabItem
173
173
  {tabItem}
174
+ {pathPrefix}
174
175
  isSelected={index === selectedTabIndex}
175
176
  {textColor}
176
177
  {selectedTextColor}
@@ -11,6 +11,7 @@
11
11
  export type TabItemProps = {
12
12
  // 基本プロパティ
13
13
  tabItem: MenuItem;
14
+ pathPrefix?: string;
14
15
 
15
16
  // スタイル/レイアウト
16
17
  textColor: string;
@@ -31,6 +32,7 @@
31
32
  let {
32
33
  // 基本プロパティ
33
34
  tabItem,
35
+ pathPrefix = '',
34
36
 
35
37
  // スタイル/レイアウト
36
38
  textColor,
@@ -47,10 +49,29 @@
47
49
  // 状態/動作
48
50
  isSelected = false
49
51
  }: TabItemProps = $props();
52
+
53
+ // =========================================================================
54
+ // $derived
55
+ // =========================================================================
56
+
57
+ // pathPrefixを付与したhrefを計算
58
+ const hrefWithPrefix = $derived.by(() => {
59
+ if (!tabItem.href) return undefined;
60
+ if (!pathPrefix) return tabItem.href;
61
+
62
+ // 既にpathPrefixが含まれている場合はそのまま
63
+ // pathPrefixが完全一致、またはpathPrefix + '/'で始まる場合
64
+ if (tabItem.href === pathPrefix || tabItem.href.startsWith(`${pathPrefix}/`)) {
65
+ return tabItem.href;
66
+ }
67
+
68
+ // pathPrefixを付与
69
+ return `${pathPrefix}${tabItem.href.startsWith('/') ? '' : '/'}${tabItem.href}`;
70
+ });
50
71
  </script>
51
72
 
52
73
  <a
53
- href={tabItem.href}
74
+ href={hrefWithPrefix}
54
75
  class="tab-item"
55
76
  class:tab-item--selected={isSelected}
56
77
  style="--svelte-ui-tab-item-text-color: {textColor}; --svelte-ui-tab-item-selected-text-color: {selectedTextColor}; --svelte-ui-tab-item-selected-bar-color: {selectedBarColor}"
@@ -2,6 +2,7 @@ import type { MenuItem } from '../types/menuItem';
2
2
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
3
  export type TabItemProps = {
4
4
  tabItem: MenuItem;
5
+ pathPrefix?: string;
5
6
  textColor: string;
6
7
  selectedTextColor: string;
7
8
  selectedBarColor: string;
@@ -180,6 +180,8 @@
180
180
  }: TextareaProps = $props();
181
181
 
182
182
  let ref: HTMLTextAreaElement | null = null;
183
+ let displayTextRef: HTMLDivElement | null = $state(null);
184
+ let linkTextRef: HTMLDivElement | null = $state(null);
183
185
  let isFocused: boolean = $state(false);
184
186
 
185
187
  // =========================================================================
@@ -330,6 +332,18 @@
330
332
  onpointercancel?.(event);
331
333
  };
332
334
 
335
+ // スクロール同期
336
+ const handleScroll = () => {
337
+ if (!ref) return;
338
+ const scrollTop = ref.scrollTop;
339
+ if (displayTextRef) {
340
+ displayTextRef.scrollTop = scrollTop;
341
+ }
342
+ if (linkify && linkTextRef) {
343
+ linkTextRef.scrollTop = scrollTop;
344
+ }
345
+ };
346
+
333
347
  // =========================================================================
334
348
  // $derived
335
349
  // =========================================================================
@@ -388,13 +402,14 @@
388
402
  >
389
403
  <!-- autoResize時の表示用要素(HTMLレンダリングで高さ調整) -->
390
404
  <div
405
+ bind:this={displayTextRef}
391
406
  class="textarea__display-text"
392
407
  data-placeholder={placeholder}
393
- style="min-height: {minHeightStyle}; {customStyle}"
408
+ style="min-height: {minHeightStyle}; max-height: {maxHeightStyle}; {customStyle}"
394
409
  >
395
410
  {@html htmlValue}
396
411
  </div>
397
- <div class="textarea__input">
412
+ <div class="textarea__wrapper">
398
413
  <textarea
399
414
  {id}
400
415
  {name}
@@ -438,32 +453,37 @@
438
453
  onpointerleave={handlePointerLeave}
439
454
  onpointermove={handlePointerMove}
440
455
  onpointercancel={handlePointerCancel}
456
+ onscroll={handleScroll}
441
457
  {...textareaAttributes}
442
458
  {...restProps}
443
459
  ></textarea>
444
- <!-- クリアボタン -->
445
- {#if clearable && !disabled && !readonly}
446
- <div class="textarea__clear-button">
447
- <IconButton
448
- ariaLabel={clearButtonAriaLabel}
449
- color="var(--svelte-ui-textarea-text-color)"
450
- onclick={(event) => {
451
- event.stopPropagation();
452
- clear();
453
- }}
454
- tabindex={-1}
455
- iconFilled={true}
456
- {iconVariant}
457
- fontSize={18}>cancel</IconButton
458
- >
459
- </div>
460
- {/if}
461
460
  </div>
462
461
  {#if linkify}
463
- <div class="textarea__link-text" style="min-height: {minHeightStyle}; {customStyle}">
462
+ <div
463
+ bind:this={linkTextRef}
464
+ class="textarea__link-text"
465
+ style="min-height: {minHeightStyle}; max-height: {maxHeightStyle}; {customStyle}"
466
+ >
464
467
  {@html linkHtmlValue}
465
468
  </div>
466
469
  {/if}
470
+ <!-- クリアボタン -->
471
+ {#if clearable && !disabled && !readonly}
472
+ <div class="textarea__clear-button">
473
+ <IconButton
474
+ ariaLabel={clearButtonAriaLabel}
475
+ color="var(--svelte-ui-text-color)"
476
+ onclick={(event) => {
477
+ event.stopPropagation();
478
+ clear();
479
+ }}
480
+ tabindex={-1}
481
+ iconFilled={true}
482
+ {iconVariant}
483
+ fontSize={18}>cancel</IconButton
484
+ >
485
+ </div>
486
+ {/if}
467
487
  </div>
468
488
 
469
489
  <style>
@@ -481,7 +501,7 @@
481
501
  }
482
502
  }
483
503
 
484
- .textarea__input {
504
+ .textarea__wrapper {
485
505
  position: absolute;
486
506
  top: 0;
487
507
  left: 0;
@@ -514,6 +534,14 @@
514
534
  opacity: 1;
515
535
  transition: none;
516
536
  cursor: text !important;
537
+ overflow-y: auto;
538
+ overflow-x: hidden;
539
+ scrollbar-width: none; /* Firefox */
540
+ -ms-overflow-style: none; /* IE and Edge */
541
+
542
+ &::-webkit-scrollbar {
543
+ display: none; /* Chrome, Safari, Opera */
544
+ }
517
545
 
518
546
  &::before {
519
547
  content: '';
@@ -602,7 +630,8 @@
602
630
  * ============================================= */
603
631
  .textarea--clearable {
604
632
  textarea,
605
- .textarea__display-text {
633
+ .textarea__display-text,
634
+ .textarea__link-text {
606
635
  padding-right: var(--svelte-ui-textarea-icon-space);
607
636
  }
608
637
  }
@@ -688,9 +717,13 @@
688
717
  opacity: 0;
689
718
  }
690
719
 
691
- /* フォーカス時はリンク用オーバーレイも非表示にして(display:none)、リンクが反応しないようにする */
720
+ /* フォーカス時はリンク用オーバーレイも非表示(opacity: 0)にして、リンクが反応しないようにする */
692
721
  .textarea--focused .textarea__link-text {
693
- display: none;
722
+ opacity: 0;
723
+ }
724
+
725
+ .textarea--focused .textarea__link-text :global(a) {
726
+ pointer-events: none;
694
727
  }
695
728
 
696
729
  /* =============================================
@@ -710,11 +743,12 @@
710
743
  box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-textarea-border-color);
711
744
  border: none;
712
745
  border-radius: var(--svelte-ui-textarea-border-radius);
713
- color: var(--svelte-ui-textarea-text-color);
714
746
  }
715
747
 
716
748
  &.textarea--clearable {
717
- textarea {
749
+ textarea,
750
+ .textarea__display-text,
751
+ .textarea__link-text {
718
752
  padding-right: var(--svelte-ui-textarea-icon-space);
719
753
  }
720
754
  }
@@ -215,5 +215,4 @@ type NestedKeyOf<T> = T extends object ? {
215
215
  [K in keyof T]: K extends string ? T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : K : never;
216
216
  }[keyof T] : never;
217
217
  export declare const t: (key: NestedKeyOf<typeof TRANSLATIONS.en>, params?: Record<string, any>) => string;
218
- export declare const debugLocale: () => Locale;
219
218
  export {};
@@ -28,20 +28,10 @@ export const t = (key, params) => {
28
28
  const locale = (globalLocale && globalLocale in TRANSLATIONS) ? globalLocale : 'en';
29
29
  const message = key.split('.').reduce((obj, k) => obj?.[k], TRANSLATIONS[locale]);
30
30
  if (typeof message !== 'string') {
31
- console.warn(`Translation key "${key}" not found for locale "${locale}"`);
31
+ if (import.meta.env.DEV) {
32
+ console.warn(`Translation key "${key}" not found for locale "${locale}"`);
33
+ }
32
34
  return key;
33
35
  }
34
36
  return replaceParams(message, params);
35
37
  };
36
- // デバッグ用: 現在の言語設定を確認
37
- export const debugLocale = () => {
38
- if (typeof navigator !== 'undefined') {
39
- console.log('navigator.language:', navigator.language);
40
- console.log('navigator.languages:', navigator.languages);
41
- }
42
- const globalResolved = getLocale();
43
- // グローバル設定のロケールが TRANSLATIONS に存在する場合はそれを使い、存在しない場合は 'en' にフォールバック
44
- const effective = (globalResolved && globalResolved in TRANSLATIONS) ? globalResolved : 'en';
45
- console.log('effective locale (i18n):', effective);
46
- return effective;
47
- };
@@ -1,4 +1,4 @@
1
- export declare const convertToHtml: (value: string | number | null) => string | number | null;
1
+ export declare const convertToHtml: (value: string | number) => string;
2
2
  export declare const convertToLink: (str: string) => string;
3
3
  export declare const escapeHtml: (value: string) => string;
4
- export declare const convertToHtmlWithLink: (value: string | number | null) => string | number | null;
4
+ export declare const convertToHtmlWithLink: (value: string | number) => string;
@@ -13,7 +13,7 @@ export const convertToHtml = (value) => {
13
13
  return html;
14
14
  }
15
15
  else {
16
- return value;
16
+ return String(value);
17
17
  }
18
18
  };
19
19
  export const convertToLink = (str) => {
@@ -42,6 +42,6 @@ export const convertToHtmlWithLink = (value) => {
42
42
  return htmlWithLink;
43
43
  }
44
44
  else {
45
- return value;
45
+ return String(value);
46
46
  }
47
47
  };
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.14",
5
+ "version": "0.0.15",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",