compote-ui 0.60.6 → 0.61.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.
@@ -6,7 +6,8 @@ const TYPE_DEFAULTS = {
6
6
  time: { align: 'center', size: 80 },
7
7
  'date-time': { align: 'center', size: 160 },
8
8
  boolean: { align: 'center', size: 90 },
9
- url: { align: 'center', size: 60, enableSorting: false }
9
+ url: { align: 'center', size: 60, enableSorting: false },
10
+ phone: { align: 'left', size: 160 }
10
11
  };
11
12
  function applyTypeDefaults(options) {
12
13
  const defaults = options.type ? TYPE_DEFAULTS[options.type] : undefined;
@@ -2,15 +2,18 @@
2
2
  import type { Cell, RowData } from '@tanstack/svelte-table';
3
3
  import { FlexRender } from '@tanstack/svelte-table';
4
4
  import { cn } from 'tailwind-variants';
5
- import { PhArrowSquareOut, PhCheck, PhX } from '../../icons';
5
+ import { PhArrowSquareOut, PhCheck, PhPhone, PhX } from '../../icons';
6
6
  import type { DataTableFeatures } from './features';
7
7
  import {
8
8
  getBooleanCellValue,
9
9
  getColumnMeta,
10
+ getPhoneCellValue,
10
11
  getUrlCellValue,
11
12
  justifyClass,
13
+ openPhoneCell,
12
14
  openUrlCell
13
15
  } from './data-table-utils';
16
+ import { formatPhoneCellValue } from './phone-format';
14
17
 
15
18
  type Props = {
16
19
  cell: Cell<DataTableFeatures, T, unknown>;
@@ -61,6 +64,26 @@
61
64
  {:else}
62
65
  -
63
66
  {/if}
67
+ {:else if meta?.type === 'phone'}
68
+ {@const value = getPhoneCellValue(cell.getValue())}
69
+ {#if value}
70
+ <span class={cn('inline-flex max-w-full items-center gap-1.5', justifyClass(meta.align))}>
71
+ <button
72
+ type="button"
73
+ class="inline-flex shrink-0 appearance-none rounded-sm border-0 bg-transparent p-0 text-ink-dim outline-none hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
74
+ aria-label="Call {value}"
75
+ onclick={(event) => {
76
+ event.stopPropagation();
77
+ openPhoneCell(value);
78
+ }}
79
+ >
80
+ <PhPhone class="size-3.5" />
81
+ </button>
82
+ <span class="truncate leading-5">{formatPhoneCellValue(value)}</span>
83
+ </span>
84
+ {:else}
85
+ -
86
+ {/if}
64
87
  {:else}
65
88
  <FlexRender {cell} />
66
89
  {/if}
@@ -96,6 +96,8 @@ export declare function getPinningStyle<T extends RowData>(column: Column<DataTa
96
96
  export declare function getGroupPinningStyle<T extends RowData>(header: Header<DataTableFeatures, T, unknown>, section: 'left' | 'center' | 'right' | undefined, state: DataTableTrackedState, isRowSelectionEnabled?: boolean): string | undefined;
97
97
  export declare function getUrlCellValue(value: unknown): string | undefined;
98
98
  export declare function openUrlCell(value: string): void;
99
+ export declare function getPhoneCellValue(value: unknown): string | undefined;
100
+ export declare function openPhoneCell(value: string): void;
99
101
  export declare function getColumnMeta(columnDef: {
100
102
  meta?: DataTableColumnMeta;
101
103
  }): DataTableColumnMeta | undefined;
@@ -146,6 +146,14 @@ export function getUrlCellValue(value) {
146
146
  export function openUrlCell(value) {
147
147
  window.open(value, '_blank', 'noopener,noreferrer');
148
148
  }
149
+ export function getPhoneCellValue(value) {
150
+ if (typeof value !== 'string' || value.trim() === '')
151
+ return undefined;
152
+ return value;
153
+ }
154
+ export function openPhoneCell(value) {
155
+ window.location.href = `tel:${value}`;
156
+ }
149
157
  // `columnDef.meta` is natively typed as DataTableColumnMeta via the `columnMeta`
150
158
  // type-only slot in features.ts; this accessor just narrows the columnDef shape.
151
159
  export function getColumnMeta(columnDef) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Generic, country-agnostic readability formatter for E.164-ish phone strings
3
+ * (e.g. `+381601234567` -> `+381 601234567`). Splits off the country calling
4
+ * code using a static ITU code table and adds a single space after it; the
5
+ * subscriber number itself is left ungrouped, since grouping it correctly
6
+ * requires per-country number-plan knowledge (trunk codes, area codes, etc.)
7
+ * that doesn't belong in a generic default — apply a `cell` callback for
8
+ * locale-accurate national formatting. Values that don't start with `+` are
9
+ * returned unchanged.
10
+ */
11
+ export declare function formatPhoneCellValue(value: string): string;
@@ -0,0 +1,242 @@
1
+ // E.164 country calling codes (digits only, no leading '+'). Sourced once from
2
+ // svelte-tel-input's country list — kept as a plain literal so the data-table
3
+ // subpath never imports svelte-tel-input (and its libphonenumber-js dependency)
4
+ // at runtime.
5
+ const CALLING_CODES = new Set([
6
+ '1',
7
+ '7',
8
+ '20',
9
+ '27',
10
+ '30',
11
+ '31',
12
+ '32',
13
+ '33',
14
+ '34',
15
+ '36',
16
+ '39',
17
+ '40',
18
+ '41',
19
+ '43',
20
+ '44',
21
+ '45',
22
+ '46',
23
+ '47',
24
+ '48',
25
+ '49',
26
+ '51',
27
+ '52',
28
+ '53',
29
+ '54',
30
+ '55',
31
+ '56',
32
+ '57',
33
+ '58',
34
+ '60',
35
+ '61',
36
+ '62',
37
+ '63',
38
+ '64',
39
+ '65',
40
+ '66',
41
+ '81',
42
+ '82',
43
+ '84',
44
+ '86',
45
+ '90',
46
+ '91',
47
+ '92',
48
+ '93',
49
+ '94',
50
+ '95',
51
+ '98',
52
+ '211',
53
+ '212',
54
+ '213',
55
+ '216',
56
+ '218',
57
+ '220',
58
+ '221',
59
+ '222',
60
+ '223',
61
+ '224',
62
+ '225',
63
+ '226',
64
+ '227',
65
+ '228',
66
+ '229',
67
+ '230',
68
+ '231',
69
+ '232',
70
+ '233',
71
+ '234',
72
+ '235',
73
+ '236',
74
+ '237',
75
+ '238',
76
+ '239',
77
+ '240',
78
+ '241',
79
+ '242',
80
+ '243',
81
+ '244',
82
+ '245',
83
+ '246',
84
+ '247',
85
+ '248',
86
+ '249',
87
+ '250',
88
+ '251',
89
+ '252',
90
+ '253',
91
+ '254',
92
+ '255',
93
+ '256',
94
+ '257',
95
+ '258',
96
+ '260',
97
+ '261',
98
+ '262',
99
+ '263',
100
+ '264',
101
+ '265',
102
+ '266',
103
+ '267',
104
+ '268',
105
+ '269',
106
+ '290',
107
+ '291',
108
+ '297',
109
+ '298',
110
+ '299',
111
+ '350',
112
+ '351',
113
+ '352',
114
+ '353',
115
+ '354',
116
+ '355',
117
+ '356',
118
+ '357',
119
+ '358',
120
+ '359',
121
+ '370',
122
+ '371',
123
+ '372',
124
+ '373',
125
+ '374',
126
+ '375',
127
+ '376',
128
+ '377',
129
+ '378',
130
+ '380',
131
+ '381',
132
+ '382',
133
+ '383',
134
+ '385',
135
+ '386',
136
+ '387',
137
+ '389',
138
+ '420',
139
+ '421',
140
+ '423',
141
+ '500',
142
+ '501',
143
+ '502',
144
+ '503',
145
+ '504',
146
+ '505',
147
+ '506',
148
+ '507',
149
+ '508',
150
+ '509',
151
+ '590',
152
+ '591',
153
+ '592',
154
+ '593',
155
+ '594',
156
+ '595',
157
+ '596',
158
+ '597',
159
+ '598',
160
+ '599',
161
+ '670',
162
+ '672',
163
+ '673',
164
+ '674',
165
+ '675',
166
+ '676',
167
+ '677',
168
+ '678',
169
+ '679',
170
+ '680',
171
+ '681',
172
+ '682',
173
+ '683',
174
+ '685',
175
+ '686',
176
+ '687',
177
+ '688',
178
+ '689',
179
+ '690',
180
+ '691',
181
+ '692',
182
+ '850',
183
+ '852',
184
+ '853',
185
+ '855',
186
+ '856',
187
+ '880',
188
+ '886',
189
+ '960',
190
+ '961',
191
+ '962',
192
+ '963',
193
+ '964',
194
+ '965',
195
+ '966',
196
+ '967',
197
+ '968',
198
+ '970',
199
+ '971',
200
+ '972',
201
+ '973',
202
+ '974',
203
+ '975',
204
+ '976',
205
+ '977',
206
+ '992',
207
+ '993',
208
+ '994',
209
+ '995',
210
+ '996',
211
+ '998'
212
+ ]);
213
+ function matchCallingCode(digits) {
214
+ for (const len of [3, 2, 1]) {
215
+ const candidate = digits.slice(0, len);
216
+ if (candidate.length === len && CALLING_CODES.has(candidate))
217
+ return candidate;
218
+ }
219
+ return undefined;
220
+ }
221
+ /**
222
+ * Generic, country-agnostic readability formatter for E.164-ish phone strings
223
+ * (e.g. `+381601234567` -> `+381 601234567`). Splits off the country calling
224
+ * code using a static ITU code table and adds a single space after it; the
225
+ * subscriber number itself is left ungrouped, since grouping it correctly
226
+ * requires per-country number-plan knowledge (trunk codes, area codes, etc.)
227
+ * that doesn't belong in a generic default — apply a `cell` callback for
228
+ * locale-accurate national formatting. Values that don't start with `+` are
229
+ * returned unchanged.
230
+ */
231
+ export function formatPhoneCellValue(value) {
232
+ if (!value.startsWith('+'))
233
+ return value;
234
+ const digits = value.slice(1).replace(/\D/g, '');
235
+ if (digits === '')
236
+ return value;
237
+ const code = matchCallingCode(digits);
238
+ if (!code)
239
+ return `+${digits}`;
240
+ const rest = digits.slice(code.length);
241
+ return rest ? `+${code} ${rest}` : `+${code}`;
242
+ }
@@ -2,7 +2,7 @@ import type { FilterFnOption, RowData } from '@tanstack/svelte-table';
2
2
  import type { Component, Snippet } from 'svelte';
3
3
  import type { DataTableFeatures } from './features';
4
4
  export type DataTableAlign = 'left' | 'center' | 'right';
5
- export type DataTableColumnType = 'text' | 'number' | 'currency' | 'percent' | 'boolean' | 'select' | 'url' | 'date' | 'time' | 'date-time';
5
+ export type DataTableColumnType = 'text' | 'number' | 'currency' | 'percent' | 'boolean' | 'select' | 'url' | 'phone' | 'date' | 'time' | 'date-time';
6
6
  export type DataTableCellRenderer<T extends RowData> = (value: unknown, row: T) => string | number | boolean | null | undefined;
7
7
  export type DataTableCellRenderProps<T extends RowData> = {
8
8
  value: unknown;
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import { Select } from '@ark-ui/svelte/select';
3
+ import { Portal } from '@ark-ui/svelte/portal';
4
+ import { TelInput, countries } from 'svelte-tel-input';
5
+ import type { CountryCode } from 'svelte-tel-input/types';
6
+ import type { PhoneInputProps } from './types';
7
+ import { createListCollection } from '../../utils/collections';
8
+ import { cn } from 'tailwind-variants';
9
+ import { PhCaretDown, PhCheck } from '../../icons';
10
+ import 'svelte-tel-input/styles/flags.css';
11
+
12
+ interface CountryItem {
13
+ value: string;
14
+ label: string;
15
+ flagClass: string;
16
+ dialCode: string;
17
+ }
18
+
19
+ const countryItems: CountryItem[] = countries.map((c) => ({
20
+ value: c.iso2,
21
+ label: `${c.name} +${c.dialCode}`,
22
+ flagClass: `flag-${c.iso2.toLowerCase()}`,
23
+ dialCode: c.dialCode
24
+ }));
25
+ const collection = createListCollection(countryItems);
26
+
27
+ let {
28
+ value = $bindable(''),
29
+ country = $bindable(null),
30
+ valid = $bindable(false),
31
+ label,
32
+ placeholder,
33
+ name,
34
+ layout = 'vertical',
35
+ disabled,
36
+ readonly,
37
+ invalid,
38
+ required,
39
+ class: className,
40
+ options
41
+ }: PhoneInputProps = $props();
42
+
43
+ const id = $props.id();
44
+ const numberId = `${id}-number`;
45
+
46
+ const selectedItem = $derived(countryItems.find((c) => c.value === country));
47
+
48
+ const rootClass = $derived(
49
+ layout === 'horizontal'
50
+ ? 'flex items-center justify-between gap-1.5 w-full'
51
+ : 'flex flex-col gap-1.5 w-full max-w-72'
52
+ );
53
+ </script>
54
+
55
+ <div class={rootClass}>
56
+ {#if label}
57
+ <label for={numberId} class="text-sm leading-none font-medium">
58
+ {label}
59
+ {#if required}<span class="text-danger">*</span>{/if}
60
+ </label>
61
+ {/if}
62
+ <div
63
+ class={cn(
64
+ 'flex h-9 w-full items-center rounded-md border bg-surface-1 shadow-sm transition-shadow focus-within:ring-1 focus-within:ring-ring',
65
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50',
66
+ invalid && 'border-danger focus-within:ring-danger',
67
+ className
68
+ )}
69
+ data-disabled={disabled ? '' : undefined}
70
+ >
71
+ <Select.Root
72
+ {collection}
73
+ value={country ? [country] : []}
74
+ onValueChange={(details) => {
75
+ country = (details.items[0]?.value as CountryCode | undefined) ?? null;
76
+ }}
77
+ {disabled}
78
+ readOnly={readonly}
79
+ class="h-9 shrink-0"
80
+ >
81
+ <Select.Control class="h-full">
82
+ <Select.Trigger
83
+ aria-label="Country"
84
+ class="flex h-9 cursor-pointer items-center gap-1.5 rounded-l-md border-r px-2.5 text-sm outline-none transition-colors hover:bg-surface-2 active:bg-surface-2 data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent"
85
+ >
86
+ {#if selectedItem}
87
+ <span
88
+ class="flag {selectedItem.flagClass} shrink-0 rounded-xs shadow-[inset_0_0_0_1px_var(--compote-border)]"
89
+ ></span>
90
+ <span class="font-medium tabular-nums">+{selectedItem.dialCode}</span>
91
+ {:else}
92
+ <span class="text-ink-dim">Country</span>
93
+ {/if}
94
+ <PhCaretDown class="size-3 shrink-0 text-ink-dim" />
95
+ </Select.Trigger>
96
+ </Select.Control>
97
+ <Portal>
98
+ <Select.Positioner>
99
+ <Select.Content
100
+ class="z-200 max-h-60 min-w-52 overflow-auto rounded-md border bg-surface-document p-1 shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95"
101
+ >
102
+ {#each countryItems as item (item.value)}
103
+ <Select.Item
104
+ {item}
105
+ class="relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm text-ink transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-surface-1 data-[state=checked]:bg-surface-1"
106
+ >
107
+ <span
108
+ class="flag {item.flagClass} shrink-0 rounded-xs shadow-[inset_0_0_0_1px_var(--compote-border)]"
109
+ ></span>
110
+ <Select.ItemText class="flex-1 truncate">{item.label}</Select.ItemText>
111
+ <Select.ItemIndicator class="absolute right-2 flex items-center justify-center">
112
+ <PhCheck class="size-3.5" />
113
+ </Select.ItemIndicator>
114
+ </Select.Item>
115
+ {/each}
116
+ </Select.Content>
117
+ </Select.Positioner>
118
+ </Portal>
119
+ </Select.Root>
120
+ <TelInput
121
+ id={numberId}
122
+ bind:value
123
+ bind:country
124
+ bind:valid
125
+ {name}
126
+ {placeholder}
127
+ {disabled}
128
+ {readonly}
129
+ {required}
130
+ {options}
131
+ class="h-full min-w-0 flex-1 bg-transparent px-3 text-sm outline-none placeholder:text-ink-dim disabled:cursor-not-allowed"
132
+ />
133
+ </div>
134
+ </div>
@@ -0,0 +1,5 @@
1
+ import type { PhoneInputProps } from './types';
2
+ import 'svelte-tel-input/styles/flags.css';
3
+ declare const PhoneInput: import("svelte").Component<PhoneInputProps, {}, "value" | "country" | "valid">;
4
+ type PhoneInput = ReturnType<typeof PhoneInput>;
5
+ export default PhoneInput;
@@ -0,0 +1,16 @@
1
+ import type { CountryCode, TelInputOptions } from 'svelte-tel-input/types';
2
+ export interface PhoneInputProps {
3
+ value?: string;
4
+ country?: CountryCode | null;
5
+ valid?: boolean;
6
+ label?: string;
7
+ placeholder?: string;
8
+ name?: string;
9
+ layout?: 'vertical' | 'horizontal';
10
+ disabled?: boolean;
11
+ readonly?: boolean;
12
+ invalid?: boolean;
13
+ required?: boolean;
14
+ class?: string;
15
+ options?: TelInputOptions;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ let { class: className = '', ...restProps } = $props();
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="0 0 256 256"
10
+ class={className}
11
+ {...restProps}
12
+ >
13
+ <path
14
+ fill="currentColor"
15
+ d="M231.88,175.08A56.26,56.26,0,0,1,176,224C96.6,224,32,159.4,32,80A56.26,56.26,0,0,1,80.92,24.12a16,16,0,0,1,16.62,9.52l21.12,47.15,0,.12A16,16,0,0,1,117.39,96c-.18.27-.37.52-.57.77L96,121.45c7.49,15.22,23.41,31,38.83,38.51l24.34-20.71a8.12,8.12,0,0,1,.75-.56,16,16,0,0,1,15.17-1.4l.13.06,47.11,21.11A16,16,0,0,1,231.88,175.08Z"
16
+ />
17
+ </svg>
18
+ <!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
@@ -0,0 +1,5 @@
1
+ declare const PhPhone: import("svelte").Component<{
2
+ class?: string;
3
+ } & Record<string, any>, {}, "">;
4
+ type PhPhone = ReturnType<typeof PhPhone>;
5
+ export default PhPhone;
@@ -18,6 +18,7 @@ export { default as PhListMagnifyingGlass } from './PhListMagnifyingGlass.svelte
18
18
  export { default as PhMagnifyingGlass } from './PhMagnifyingGlass.svelte';
19
19
  export { default as PhMicrosoftExcelLogo } from './PhMicrosoftExcelLogo.svelte';
20
20
  export { default as PhMinus } from './PhMinus.svelte';
21
+ export { default as PhPhone } from './PhPhone.svelte';
21
22
  export { default as PhStar } from './PhStar.svelte';
22
23
  export { default as PhUploadSimple } from './PhUploadSimple.svelte';
23
24
  export { default as PhUser } from './PhUser.svelte';
@@ -18,6 +18,7 @@ export { default as PhListMagnifyingGlass } from './PhListMagnifyingGlass.svelte
18
18
  export { default as PhMagnifyingGlass } from './PhMagnifyingGlass.svelte';
19
19
  export { default as PhMicrosoftExcelLogo } from './PhMicrosoftExcelLogo.svelte';
20
20
  export { default as PhMinus } from './PhMinus.svelte';
21
+ export { default as PhPhone } from './PhPhone.svelte';
21
22
  export { default as PhStar } from './PhStar.svelte';
22
23
  export { default as PhUploadSimple } from './PhUploadSimple.svelte';
23
24
  export { default as PhUser } from './PhUser.svelte';
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export { default as JsonTreeView } from './components/json-tree-view/json-tree-v
37
37
  export * as Listbox from './components/listbox';
38
38
  export { default as NumberInput } from './components/number-input/number-input.svelte';
39
39
  export type { NumberInputProps } from './components/number-input/types';
40
+ export { default as PhoneInput } from './components/phone-input/phone-input.svelte';
41
+ export type { PhoneInputProps } from './components/phone-input/types';
40
42
  export * as Popover from './components/popover';
41
43
  export { default as QrCode } from './components/qr-code/qr-code.svelte';
42
44
  export { default as Select } from './components/select/select.svelte';
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ export { default as ImageCropDialog } from './components/image-crop-dialog/image
27
27
  export { default as JsonTreeView } from './components/json-tree-view/json-tree-view.svelte';
28
28
  export * as Listbox from './components/listbox';
29
29
  export { default as NumberInput } from './components/number-input/number-input.svelte';
30
+ export { default as PhoneInput } from './components/phone-input/phone-input.svelte';
30
31
  export * as Popover from './components/popover';
31
32
  export { default as QrCode } from './components/qr-code/qr-code.svelte';
32
33
  export { default as Select } from './components/select/select.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.60.6",
3
+ "version": "0.61.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev --open",
@@ -58,13 +58,13 @@
58
58
  "@iconify-json/ph": "^1.2.2",
59
59
  "@sveltejs/adapter-auto": "^7.0.0",
60
60
  "@sveltejs/adapter-cloudflare": "^7.2.9",
61
- "@sveltejs/kit": "^2.65.2",
61
+ "@sveltejs/kit": "^2.66.0",
62
62
  "@sveltejs/package": "^2.5.8",
63
63
  "@sveltejs/vite-plugin-svelte": "7.1.2",
64
64
  "@tailwindcss/vite": "^4.3.1",
65
- "@tanstack/svelte-table": "^9.0.0-beta.12",
65
+ "@tanstack/svelte-table": "^9.0.0-beta.17",
66
66
  "@tanstack/svelte-virtual": "^3.13.29",
67
- "@types/node": "^22.19.18",
67
+ "@types/node": "^22.20.0",
68
68
  "eslint": "^10.5.0",
69
69
  "eslint-config-prettier": "^10.1.8",
70
70
  "eslint-plugin-svelte": "^3.19.0",
@@ -90,6 +90,7 @@
90
90
  "@fontsource-variable/nunito-sans": "^5.2.7",
91
91
  "@internationalized/date": "^3.12.2",
92
92
  "runed": "^0.37.1",
93
+ "svelte-tel-input": "^4.2.0",
93
94
  "tailwind-merge": "^3.6.0",
94
95
  "tailwind-variants": "^3.2.2"
95
96
  }