@htlkg/components 0.0.1
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.
- package/dist/composables/index.js +388 -0
- package/dist/composables/index.js.map +1 -0
- package/package.json +41 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useForm.test.ts +229 -0
- package/src/composables/useForm.ts +130 -0
- package/src/composables/useFormValidation.test.ts +189 -0
- package/src/composables/useFormValidation.ts +83 -0
- package/src/composables/useModal.property.test.ts +164 -0
- package/src/composables/useModal.ts +43 -0
- package/src/composables/useNotifications.test.ts +166 -0
- package/src/composables/useNotifications.ts +81 -0
- package/src/composables/useTable.property.test.ts +198 -0
- package/src/composables/useTable.ts +134 -0
- package/src/composables/useTabs.property.test.ts +247 -0
- package/src/composables/useTabs.ts +101 -0
- package/src/data/Chart.demo.vue +340 -0
- package/src/data/Chart.md +525 -0
- package/src/data/Chart.vue +133 -0
- package/src/data/DataList.md +80 -0
- package/src/data/DataList.test.ts +69 -0
- package/src/data/DataList.vue +46 -0
- package/src/data/SearchableSelect.md +107 -0
- package/src/data/SearchableSelect.vue +124 -0
- package/src/data/Table.demo.vue +296 -0
- package/src/data/Table.md +588 -0
- package/src/data/Table.property.test.ts +548 -0
- package/src/data/Table.test.ts +562 -0
- package/src/data/Table.unit.test.ts +544 -0
- package/src/data/Table.vue +321 -0
- package/src/data/index.ts +5 -0
- package/src/domain/BrandCard.md +81 -0
- package/src/domain/BrandCard.vue +63 -0
- package/src/domain/BrandSelector.md +84 -0
- package/src/domain/BrandSelector.vue +65 -0
- package/src/domain/ProductBadge.md +60 -0
- package/src/domain/ProductBadge.vue +47 -0
- package/src/domain/UserAvatar.md +84 -0
- package/src/domain/UserAvatar.vue +60 -0
- package/src/domain/domain-components.property.test.ts +449 -0
- package/src/domain/index.ts +4 -0
- package/src/forms/DateRange.demo.vue +273 -0
- package/src/forms/DateRange.md +337 -0
- package/src/forms/DateRange.vue +110 -0
- package/src/forms/JsonSchemaForm.demo.vue +549 -0
- package/src/forms/JsonSchemaForm.md +112 -0
- package/src/forms/JsonSchemaForm.property.test.ts +817 -0
- package/src/forms/JsonSchemaForm.test.ts +601 -0
- package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
- package/src/forms/JsonSchemaForm.vue +615 -0
- package/src/forms/index.ts +3 -0
- package/src/index.ts +17 -0
- package/src/navigation/Breadcrumbs.demo.vue +142 -0
- package/src/navigation/Breadcrumbs.md +102 -0
- package/src/navigation/Breadcrumbs.test.ts +69 -0
- package/src/navigation/Breadcrumbs.vue +58 -0
- package/src/navigation/Stepper.demo.vue +337 -0
- package/src/navigation/Stepper.md +174 -0
- package/src/navigation/Stepper.vue +146 -0
- package/src/navigation/Tabs.demo.vue +293 -0
- package/src/navigation/Tabs.md +163 -0
- package/src/navigation/Tabs.test.ts +176 -0
- package/src/navigation/Tabs.vue +104 -0
- package/src/navigation/index.ts +5 -0
- package/src/overlays/Alert.demo.vue +377 -0
- package/src/overlays/Alert.md +248 -0
- package/src/overlays/Alert.test.ts +166 -0
- package/src/overlays/Alert.vue +70 -0
- package/src/overlays/Drawer.md +140 -0
- package/src/overlays/Drawer.test.ts +92 -0
- package/src/overlays/Drawer.vue +76 -0
- package/src/overlays/Modal.demo.vue +149 -0
- package/src/overlays/Modal.md +385 -0
- package/src/overlays/Modal.test.ts +128 -0
- package/src/overlays/Modal.vue +86 -0
- package/src/overlays/Notification.md +150 -0
- package/src/overlays/Notification.test.ts +96 -0
- package/src/overlays/Notification.vue +58 -0
- package/src/overlays/index.ts +4 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# DataList Component
|
|
2
|
+
|
|
3
|
+
A simple list component for displaying collections of items with customizable rendering.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Generic typing**: TypeScript support for any item type
|
|
8
|
+
- **Slot-based rendering**: Custom item templates
|
|
9
|
+
- **Loading state**: Built-in loading indicator
|
|
10
|
+
- **Empty state**: Customizable empty message
|
|
11
|
+
- **Click handling**: Item click events
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { DataList } from '@htlkg/components';
|
|
17
|
+
// or
|
|
18
|
+
import { DataList } from '@htlkg/components/data';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Prop | Type | Default | Description |
|
|
24
|
+
|------|------|---------|-------------|
|
|
25
|
+
| `items` | `T[]` | required | Array of items to display |
|
|
26
|
+
| `loading` | `boolean` | `false` | Show loading state |
|
|
27
|
+
| `emptyMessage` | `string` | `'No items to display'` | Empty state message |
|
|
28
|
+
|
|
29
|
+
## Events
|
|
30
|
+
|
|
31
|
+
| Event | Payload | Description |
|
|
32
|
+
|-------|---------|-------------|
|
|
33
|
+
| `item-click` | `T` | Item clicked |
|
|
34
|
+
|
|
35
|
+
## Slots
|
|
36
|
+
|
|
37
|
+
| Slot | Props | Description |
|
|
38
|
+
|------|-------|-------------|
|
|
39
|
+
| `default` | `{item: T, index: number}` | Custom item template |
|
|
40
|
+
|
|
41
|
+
## Exposed Methods
|
|
42
|
+
|
|
43
|
+
| Method | Description |
|
|
44
|
+
|--------|-------------|
|
|
45
|
+
| `getItems()` | Get current items array |
|
|
46
|
+
|
|
47
|
+
## Usage Examples
|
|
48
|
+
|
|
49
|
+
### Basic List
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<script setup>
|
|
53
|
+
import { DataList } from '@htlkg/components';
|
|
54
|
+
|
|
55
|
+
const items = [
|
|
56
|
+
{ id: 1, name: 'Item 1' },
|
|
57
|
+
{ id: 2, name: 'Item 2' },
|
|
58
|
+
{ id: 3, name: 'Item 3' }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const handleClick = (item) => {
|
|
62
|
+
console.log('Clicked:', item);
|
|
63
|
+
};
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<DataList
|
|
68
|
+
:items="items"
|
|
69
|
+
@item-click="handleClick"
|
|
70
|
+
>
|
|
71
|
+
<template #default="{ item }">
|
|
72
|
+
<div class="font-medium">{{ item.name }}</div>
|
|
73
|
+
</template>
|
|
74
|
+
</DataList>
|
|
75
|
+
</template>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Demo
|
|
79
|
+
|
|
80
|
+
See the [DataList demo page](/components/data-list) for interactive examples.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import DataList from './DataList.vue';
|
|
4
|
+
|
|
5
|
+
describe('DataList Component', () => {
|
|
6
|
+
const mockItems = [
|
|
7
|
+
{ id: 1, name: 'Item 1', description: 'Description 1' },
|
|
8
|
+
{ id: 2, name: 'Item 2', description: 'Description 2' },
|
|
9
|
+
{ id: 3, name: 'Item 3', description: 'Description 3' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
it('renders with basic props', () => {
|
|
13
|
+
const wrapper = mount(DataList, {
|
|
14
|
+
props: {
|
|
15
|
+
items: mockItems
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(wrapper.exists()).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders all items', () => {
|
|
23
|
+
const wrapper = mount(DataList, {
|
|
24
|
+
props: {
|
|
25
|
+
items: mockItems
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// DataList renders items in divs with cursor-pointer class
|
|
30
|
+
const items = wrapper.findAll('.cursor-pointer');
|
|
31
|
+
expect(items.length).toBe(mockItems.length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('emits item-click event when item is clicked', async () => {
|
|
35
|
+
const wrapper = mount(DataList, {
|
|
36
|
+
props: {
|
|
37
|
+
items: mockItems
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const component = wrapper.vm as any;
|
|
42
|
+
if (component.handleItemClick) {
|
|
43
|
+
component.handleItemClick(mockItems[0]);
|
|
44
|
+
expect(wrapper.emitted('item-click')).toBeTruthy();
|
|
45
|
+
expect(wrapper.emitted('item-click')?.[0]).toEqual([mockItems[0]]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles empty items array', () => {
|
|
50
|
+
const wrapper = mount(DataList, {
|
|
51
|
+
props: {
|
|
52
|
+
items: []
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(wrapper.exists()).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders with loading state', () => {
|
|
60
|
+
const wrapper = mount(DataList, {
|
|
61
|
+
props: {
|
|
62
|
+
items: mockItems,
|
|
63
|
+
loading: true
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(wrapper.exists()).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
2
|
+
interface Props {
|
|
3
|
+
items: T[];
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
emptyMessage?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
9
|
+
loading: false,
|
|
10
|
+
emptyMessage: 'No items to display'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
'item-click': [item: T];
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
// Expose methods for parent components
|
|
18
|
+
defineExpose({
|
|
19
|
+
getItems: () => props.items
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="w-full">
|
|
25
|
+
<div v-if="loading" class="p-8 text-center text-gray-600">
|
|
26
|
+
Loading...
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div v-else-if="items.length === 0" class="p-8 text-center text-gray-600">
|
|
30
|
+
{{ emptyMessage }}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div v-else class="flex flex-col gap-2">
|
|
34
|
+
<div
|
|
35
|
+
v-for="(item, index) in items"
|
|
36
|
+
:key="index"
|
|
37
|
+
class="p-4 border border-gray-200 rounded-md cursor-pointer hover:bg-gray-50 transition-colors"
|
|
38
|
+
@click="emit('item-click', item)"
|
|
39
|
+
>
|
|
40
|
+
<slot :item="item" :index="index">
|
|
41
|
+
{{ JSON.stringify(item) }}
|
|
42
|
+
</slot>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# SearchableSelect Component
|
|
2
|
+
|
|
3
|
+
A searchable dropdown select component with single and multiple selection support. Wraps `@hotelinking/ui`'s `uiSelect` and `uiSelectMultiple` components.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Single/Multiple selection**: Toggle between modes
|
|
8
|
+
- **Generic typing**: TypeScript support for any option type
|
|
9
|
+
- **Customizable keys**: Configure label and value properties
|
|
10
|
+
- **v-model support**: Two-way binding
|
|
11
|
+
- **Loading state**: Built-in loading indicator
|
|
12
|
+
- **Validation**: Error display and color states
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { SearchableSelect } from '@htlkg/components';
|
|
18
|
+
// or
|
|
19
|
+
import { SearchableSelect } from '@htlkg/components/data';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
|------|------|---------|-------------|
|
|
26
|
+
| `options` | `T[]` | required | Available options |
|
|
27
|
+
| `modelValue` | `T \| T[] \| null` | `null` | Selected value(s) (v-model) |
|
|
28
|
+
| `labelKey` | `string` | `'label'` | Property to use for display |
|
|
29
|
+
| `valueKey` | `string` | `'id'` | Property to use for value |
|
|
30
|
+
| `placeholder` | `string` | `'Select an option'` | Placeholder text |
|
|
31
|
+
| `label` | `string` | `''` | Field label |
|
|
32
|
+
| `error` | `string` | `''` | Error message |
|
|
33
|
+
| `color` | `string` | `'gray'` | Field color state |
|
|
34
|
+
| `loading` | `boolean` | `false` | Show loading state |
|
|
35
|
+
| `disabled` | `boolean` | `false` | Disable selection |
|
|
36
|
+
| `requiredText` | `string` | `''` | Required indicator |
|
|
37
|
+
| `multiple` | `boolean` | `false` | Enable multiple selection |
|
|
38
|
+
|
|
39
|
+
## Events
|
|
40
|
+
|
|
41
|
+
| Event | Payload | Description |
|
|
42
|
+
|-------|---------|-------------|
|
|
43
|
+
| `update:modelValue` | `T \| T[] \| null` | Selection changed |
|
|
44
|
+
| `change` | `T \| T[] \| null` | Selection changed |
|
|
45
|
+
|
|
46
|
+
## Exposed Methods
|
|
47
|
+
|
|
48
|
+
| Method | Description |
|
|
49
|
+
|--------|-------------|
|
|
50
|
+
| `getSelected()` | Get current selection |
|
|
51
|
+
|
|
52
|
+
## Usage Examples
|
|
53
|
+
|
|
54
|
+
### Single Selection
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<script setup>
|
|
58
|
+
import { ref } from 'vue';
|
|
59
|
+
import { SearchableSelect } from '@htlkg/components';
|
|
60
|
+
|
|
61
|
+
const selected = ref(null);
|
|
62
|
+
|
|
63
|
+
const options = [
|
|
64
|
+
{ id: 1, label: 'Option 1' },
|
|
65
|
+
{ id: 2, label: 'Option 2' },
|
|
66
|
+
{ id: 3, label: 'Option 3' }
|
|
67
|
+
];
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<SearchableSelect
|
|
72
|
+
v-model="selected"
|
|
73
|
+
:options="options"
|
|
74
|
+
label="Choose an option"
|
|
75
|
+
/>
|
|
76
|
+
</template>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Multiple Selection
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<script setup>
|
|
83
|
+
import { ref } from 'vue';
|
|
84
|
+
import { SearchableSelect } from '@htlkg/components';
|
|
85
|
+
|
|
86
|
+
const selected = ref([]);
|
|
87
|
+
|
|
88
|
+
const options = [
|
|
89
|
+
{ id: 1, label: 'Tag 1' },
|
|
90
|
+
{ id: 2, label: 'Tag 2' },
|
|
91
|
+
{ id: 3, label: 'Tag 3' }
|
|
92
|
+
];
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<template>
|
|
96
|
+
<SearchableSelect
|
|
97
|
+
v-model="selected"
|
|
98
|
+
:options="options"
|
|
99
|
+
:multiple="true"
|
|
100
|
+
label="Select tags"
|
|
101
|
+
/>
|
|
102
|
+
</template>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Demo
|
|
106
|
+
|
|
107
|
+
See the [SearchableSelect demo page](/components/searchable-select) for interactive examples.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import { uiSelect, uiSelectMultiple } from '@hotelinking/ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
options: T[];
|
|
7
|
+
modelValue?: T | T[] | null;
|
|
8
|
+
labelKey?: string;
|
|
9
|
+
valueKey?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
color?: 'primary' | 'secondary' | 'light' | 'green' | 'yellow' | 'red' | 'black' | 'gray' | 'white';
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
requiredText?: string;
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
modelValue: null,
|
|
22
|
+
labelKey: 'label',
|
|
23
|
+
valueKey: 'id',
|
|
24
|
+
placeholder: 'Select an option',
|
|
25
|
+
label: '',
|
|
26
|
+
error: '',
|
|
27
|
+
color: 'gray',
|
|
28
|
+
loading: false,
|
|
29
|
+
disabled: false,
|
|
30
|
+
requiredText: '',
|
|
31
|
+
multiple: false
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{
|
|
35
|
+
'update:modelValue': [value: T | T[] | null];
|
|
36
|
+
'change': [value: T | T[] | null];
|
|
37
|
+
}>();
|
|
38
|
+
|
|
39
|
+
// Transform options to match uiSelect/uiSelectMultiple format
|
|
40
|
+
const transformedItems = computed(() => {
|
|
41
|
+
return props.options.map(option => ({
|
|
42
|
+
id: String(option[props.valueKey]),
|
|
43
|
+
name: String(option[props.labelKey]),
|
|
44
|
+
label: option.label ? String(option.label) : undefined
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Transform modelValue to match uiSelect format (single selection)
|
|
49
|
+
const selectedItem = computed(() => {
|
|
50
|
+
if (props.multiple || !props.modelValue) {
|
|
51
|
+
return { id: '', name: '', label: undefined };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const value = props.modelValue as T;
|
|
55
|
+
return {
|
|
56
|
+
id: String(value[props.valueKey]),
|
|
57
|
+
name: String(value[props.labelKey]),
|
|
58
|
+
label: value.label ? String(value.label) : undefined
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Transform modelValue to match uiSelectMultiple format (multiple selection)
|
|
63
|
+
const selectedItems = computed(() => {
|
|
64
|
+
if (!props.multiple || !props.modelValue) return [];
|
|
65
|
+
|
|
66
|
+
const values = Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue];
|
|
67
|
+
return values.map(value => ({
|
|
68
|
+
id: String(value[props.valueKey]),
|
|
69
|
+
name: String(value[props.labelKey]),
|
|
70
|
+
label: value.label ? String(value.label) : undefined
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function handleSelectChange(selected: any) {
|
|
75
|
+
if (props.multiple) {
|
|
76
|
+
// Multiple selection: transform back to original format
|
|
77
|
+
const originalValues = selected.map((item: any) => {
|
|
78
|
+
return props.options.find(opt => String(opt[props.valueKey]) === item.id);
|
|
79
|
+
}).filter(Boolean);
|
|
80
|
+
|
|
81
|
+
emit('update:modelValue', originalValues as T[]);
|
|
82
|
+
emit('change', originalValues as T[]);
|
|
83
|
+
} else {
|
|
84
|
+
// Single selection: transform back to original format
|
|
85
|
+
const originalValue = props.options.find(opt => String(opt[props.valueKey]) === selected.id);
|
|
86
|
+
emit('update:modelValue', originalValue || null);
|
|
87
|
+
emit('change', originalValue || null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Expose methods for parent components
|
|
92
|
+
defineExpose({
|
|
93
|
+
getSelected: () => props.modelValue
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<uiSelectMultiple
|
|
99
|
+
v-if="multiple"
|
|
100
|
+
:items="transformedItems"
|
|
101
|
+
:label="label"
|
|
102
|
+
:placeholder="placeholder"
|
|
103
|
+
:error="error"
|
|
104
|
+
:color="color"
|
|
105
|
+
:select="selectedItems"
|
|
106
|
+
:loading="loading"
|
|
107
|
+
:disabled="disabled"
|
|
108
|
+
:required-text="requiredText"
|
|
109
|
+
@selectChanged="handleSelectChange"
|
|
110
|
+
/>
|
|
111
|
+
<uiSelect
|
|
112
|
+
v-else
|
|
113
|
+
:items="transformedItems"
|
|
114
|
+
:label="label"
|
|
115
|
+
:placeholder="placeholder"
|
|
116
|
+
:error="error"
|
|
117
|
+
:color="color"
|
|
118
|
+
:select="selectedItem"
|
|
119
|
+
:loading="loading"
|
|
120
|
+
:disabled="disabled"
|
|
121
|
+
:required-text="requiredText"
|
|
122
|
+
@selectChanged="handleSelectChange"
|
|
123
|
+
/>
|
|
124
|
+
</template>
|