@zap-wunschlachen/wl-shared-components 1.0.71 → 1.0.72

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/App.vue CHANGED
@@ -27,6 +27,14 @@
27
27
  >
28
28
  Select
29
29
  </button>
30
+ <button
31
+ class="nav-btn"
32
+ :class="{ active: currentPage === 'listitem' }"
33
+ type="button"
34
+ @click="currentPage = 'listitem'"
35
+ >
36
+ ListItem
37
+ </button>
30
38
  </nav>
31
39
  </header>
32
40
 
@@ -34,6 +42,7 @@
34
42
  <AllPage v-if="currentPage === 'all'" />
35
43
  <AccordionGroupPage v-else-if="currentPage === 'accordion'" />
36
44
  <SelectPage v-else-if="currentPage === 'select'" />
45
+ <ListItemPage v-else-if="currentPage === 'listitem'" />
37
46
  </main>
38
47
  </div>
39
48
  </template>
@@ -43,8 +52,9 @@ import { ref } from 'vue';
43
52
  import AllPage from '@/pages/AllPage.vue';
44
53
  import AccordionGroupPage from '@/pages/AccordionGroupPage.vue';
45
54
  import SelectPage from '@/pages/SelectPage.vue';
55
+ import ListItemPage from '@/pages/ListItemPage.vue';
46
56
 
47
- const currentPage = ref<'all' | 'accordion' | 'select'>('all');
57
+ const currentPage = ref<'all' | 'accordion' | 'select' | 'listitem'>('all');
48
58
  </script>
49
59
 
50
60
  <style scoped>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-wunschlachen/wl-shared-components",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -0,0 +1,123 @@
1
+ <template>
2
+ <v-list
3
+ ref="listRef"
4
+ class="wl-list"
5
+ :density="density"
6
+ :disabled="disabled"
7
+ :nav="nav"
8
+ :rounded="rounded"
9
+ :variant="variant"
10
+ :color="color"
11
+ :base-color="baseColor"
12
+ :active-color="activeColor"
13
+ :bg-color="bgColor"
14
+ :mandatory="mandatory"
15
+ :lines="lines"
16
+ :slim="slim"
17
+ :tag="tag"
18
+ :aria-label="ariaLabel || undefined"
19
+ v-model:selected="internalSelected"
20
+ v-model:opened="internalOpened"
21
+ data-testid="root"
22
+ >
23
+ <slot></slot>
24
+ </v-list>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { ref, watch } from 'vue';
29
+
30
+ const props = defineProps({
31
+ density: {
32
+ type: String,
33
+ default: 'default',
34
+ },
35
+ disabled: {
36
+ type: Boolean,
37
+ default: false,
38
+ },
39
+ nav: {
40
+ type: Boolean,
41
+ default: false,
42
+ },
43
+ rounded: {
44
+ type: [Boolean, String, Number],
45
+ default: undefined,
46
+ },
47
+ variant: {
48
+ type: String,
49
+ default: undefined,
50
+ },
51
+ color: {
52
+ type: String,
53
+ default: undefined,
54
+ },
55
+ baseColor: {
56
+ type: String,
57
+ default: undefined,
58
+ },
59
+ activeColor: {
60
+ type: String,
61
+ default: undefined,
62
+ },
63
+ bgColor: {
64
+ type: String,
65
+ default: undefined,
66
+ },
67
+ mandatory: {
68
+ type: Boolean,
69
+ default: false,
70
+ },
71
+ lines: {
72
+ type: [Boolean, String],
73
+ default: undefined,
74
+ },
75
+ slim: {
76
+ type: Boolean,
77
+ default: false,
78
+ },
79
+ selected: {
80
+ type: Array,
81
+ default: undefined,
82
+ },
83
+ opened: {
84
+ type: Array,
85
+ default: undefined,
86
+ },
87
+
88
+ /**
89
+ * HTML tag for the list root element.
90
+ * Use 'nav' for navigation lists, 'ul' for standard lists (WCAG 1.3.1).
91
+ * Default is Vuetify's default ('div').
92
+ */
93
+ tag: {
94
+ type: String,
95
+ default: undefined,
96
+ },
97
+
98
+ /**
99
+ * Accessible label for screen readers identifying the list's purpose.
100
+ * Especially important for navigation lists (WCAG 4.1.2).
101
+ */
102
+ ariaLabel: {
103
+ type: String,
104
+ default: undefined,
105
+ },
106
+ });
107
+
108
+ const emits = defineEmits(['update:selected', 'update:opened']);
109
+
110
+ const internalSelected = ref(props.selected);
111
+ const internalOpened = ref(props.opened);
112
+
113
+ watch(() => props.selected, (val) => { internalSelected.value = val; });
114
+ watch(() => props.opened, (val) => { internalOpened.value = val; });
115
+ watch(internalSelected, (val) => { emits('update:selected', val); });
116
+ watch(internalOpened, (val) => { emits('update:opened', val); });
117
+
118
+ const listRef = ref(null);
119
+
120
+ defineExpose({
121
+ $el: listRef,
122
+ });
123
+ </script>
@@ -16,6 +16,10 @@
16
16
  :prepend-icon="prependIcon"
17
17
  :append-icon="appendIcon"
18
18
  :ripple="ripple"
19
+ :href="href"
20
+ :to="to"
21
+ :link="link"
22
+ :aria-label="ariaLabel || undefined"
19
23
  data-testid="root"
20
24
  >
21
25
  <template v-if="$slots['default']" #default>
@@ -99,6 +103,39 @@ const props = defineProps({
99
103
  type: [Boolean, Object],
100
104
  default: undefined,
101
105
  },
106
+
107
+ /**
108
+ * URL for native link navigation. Renders the item as an <a> tag (WCAG 2.1.1).
109
+ */
110
+ href: {
111
+ type: String,
112
+ default: undefined,
113
+ },
114
+
115
+ /**
116
+ * Vue Router route object or path. Renders the item as a <router-link> (WCAG 2.1.1).
117
+ */
118
+ to: {
119
+ type: [String, Object],
120
+ default: undefined,
121
+ },
122
+
123
+ /**
124
+ * Forces the item to behave as a link (clickable/focusable) even without href/to.
125
+ */
126
+ link: {
127
+ type: Boolean,
128
+ default: undefined,
129
+ },
130
+
131
+ /**
132
+ * Accessible label for screen readers when the item's visible content
133
+ * is not descriptive enough (WCAG 4.1.2).
134
+ */
135
+ ariaLabel: {
136
+ type: String,
137
+ default: undefined,
138
+ },
102
139
  });
103
140
 
104
141
  const listItemRef = ref(null);
@@ -165,3 +165,15 @@
165
165
  opacity: 1 !important;
166
166
  --v-field-border-opacity: 1 !important;
167
167
  }
168
+
169
+ /* WCAG 2.4.7 Focus Visible — distinct focus indicator */
170
+ .wl-select .v-field--focused .v-field__outline {
171
+ color: var(--primary-color, #1976d2) !important;
172
+ --v-field-border-width: 2px !important;
173
+ }
174
+
175
+ .wl-select .v-field--focused {
176
+ outline: 2px solid var(--primary-color, #1976d2);
177
+ outline-offset: 1px;
178
+ border-radius: 8px;
179
+ }
@@ -18,7 +18,7 @@
18
18
  :clear-icon="clearIcon"
19
19
  :closable-chips="closableChips"
20
20
  :multiple="multiple"
21
- :hide-details="true"
21
+ :hide-details="hideDetails"
22
22
  :clearable="clearable"
23
23
  :items="items"
24
24
  :disabled="disabled"
@@ -29,6 +29,8 @@
29
29
  :placeholder="placeholder"
30
30
  :persistent-placeholder="persistentPlaceholder"
31
31
  :aria-invalid="error || undefined"
32
+ :aria-label="ariaLabel || undefined"
33
+ :aria-describedby="ariaDescribedby || undefined"
32
34
  :menu-props="computedMenuProps"
33
35
  v-model="internalValue"
34
36
  @click:append="onClickAppend"
@@ -277,6 +279,33 @@ const props = defineProps({
277
279
  default: false
278
280
  },
279
281
 
282
+ /**
283
+ * Controls whether validation messages (errors, hints) are displayed below the field.
284
+ * Default is true (hidden) for backwards compatibility.
285
+ * Set to false or 'auto' to show validation/error messages.
286
+ */
287
+ hideDetails: {
288
+ type: [Boolean, String],
289
+ default: true,
290
+ },
291
+
292
+ /**
293
+ * Accessible label for screen readers. Use when the visible label is empty
294
+ * or not descriptive enough (WCAG 4.1.2).
295
+ */
296
+ ariaLabel: {
297
+ type: String,
298
+ default: undefined,
299
+ },
300
+
301
+ /**
302
+ * ID of the element(s) that describe this input (e.g. error messages, hints).
303
+ * Links the input to supplementary information for screen readers (WCAG 1.3.1).
304
+ */
305
+ ariaDescribedby: {
306
+ type: String,
307
+ default: undefined,
308
+ },
280
309
  });
281
310
 
282
311
  // Define events that can be emitted by the component
@@ -165,3 +165,15 @@
165
165
  opacity: 1 !important;
166
166
  --v-field-border-opacity: 1 !important;
167
167
  }
168
+
169
+ /* WCAG 2.4.7 Focus Visible — distinct focus indicator */
170
+ .wl-select .v-field--focused .v-field__outline {
171
+ color: var(--primary-color, #1976d2) !important;
172
+ --v-field-border-width: 2px !important;
173
+ }
174
+
175
+ .wl-select .v-field--focused {
176
+ outline: 2px solid var(--primary-color, #1976d2);
177
+ outline-offset: 1px;
178
+ border-radius: 8px;
179
+ }
@@ -18,7 +18,6 @@
18
18
  :clear-icon="clearIcon"
19
19
  :closable-chips="closableChips"
20
20
  :multiple="multiple"
21
- :hide-details="true"
22
21
  :clearable="clearable"
23
22
  :items="items"
24
23
  :disabled="disabled"
@@ -30,7 +29,11 @@
30
29
  :persistent-placeholder="persistentPlaceholder"
31
30
  :auto-select-first="autoSelectFirst"
32
31
  :custom-filter="customFilter"
32
+ :hide-no-data="hideNoData"
33
+ :hide-details="hideDetails"
33
34
  :aria-invalid="error || undefined"
35
+ :aria-label="ariaLabel || undefined"
36
+ :aria-describedby="ariaDescribedby || undefined"
34
37
  :menu-props="computedMenuProps"
35
38
  v-model="internalValue"
36
39
  @click:append="onClickAppend"
@@ -302,7 +305,44 @@ const props = defineProps({
302
305
  customFilter: {
303
306
  type: Function,
304
307
  default: undefined,
305
- }
308
+ },
309
+
310
+ /**
311
+ * Hides the menu when there are no items to display.
312
+ * Default is false.
313
+ */
314
+ hideNoData: {
315
+ type: Boolean,
316
+ default: false,
317
+ },
318
+
319
+ /**
320
+ * Controls whether validation messages (errors, hints) are displayed below the field.
321
+ * Default is true (hidden) for backwards compatibility.
322
+ * Set to false or 'auto' to show validation/error messages.
323
+ */
324
+ hideDetails: {
325
+ type: [Boolean, String],
326
+ default: true,
327
+ },
328
+
329
+ /**
330
+ * Accessible label for screen readers. Use when the visible label is empty
331
+ * or not descriptive enough (WCAG 4.1.2).
332
+ */
333
+ ariaLabel: {
334
+ type: String,
335
+ default: undefined,
336
+ },
337
+
338
+ /**
339
+ * ID of the element(s) that describe this input (e.g. error messages, hints).
340
+ * Links the input to supplementary information for screen readers (WCAG 1.3.1).
341
+ */
342
+ ariaDescribedby: {
343
+ type: String,
344
+ default: undefined,
345
+ },
306
346
  });
307
347
 
308
348
  // Define events that can be emitted by the component
@@ -87,7 +87,8 @@ export { default as TicketCard } from './Laboratory/TicketCard/TicketCard.vue';
87
87
  export { default as Timeline } from './Laboratory/TimeLine/Timeline.vue';
88
88
  export { default as TimelineEvent } from './Laboratory/TimeLine/TimeLineEvent.vue';
89
89
 
90
- // ListItem
90
+ // List
91
+ export { default as List } from './List/List.vue';
91
92
  export { default as ListItem } from './ListItem/ListItem.vue';
92
93
 
93
94
  // Loader
@@ -512,92 +512,126 @@
512
512
  </td>
513
513
  </tr>
514
514
 
515
- <!-- ListItem Variants -->
515
+ <!-- List + ListItem Variants -->
516
516
  <tr>
517
- <td colspan="3" class="section-header">ListItem Variants</td>
517
+ <td colspan="3" class="section-header">List + ListItem Variants</td>
518
518
  </tr>
519
519
  <tr>
520
- <td class="variation-label">ListItem - Default</td>
520
+ <td class="variation-label">List + ListItem - Basic</td>
521
521
  <td>
522
522
  <ThemeProvider domain="domain-dental">
523
- <ListItem title="Default List Item" subtitle="Subtitle text" />
523
+ <List>
524
+ <ListItem title="First Item" subtitle="Subtitle text" />
525
+ <ListItem title="Second Item" subtitle="Another subtitle" />
526
+ <ListItem title="Third Item" subtitle="One more" />
527
+ </List>
524
528
  </ThemeProvider>
525
529
  </td>
526
530
  <td>
527
531
  <ThemeProvider domain="domain-cocoon">
528
- <ListItem title="Default List Item" subtitle="Subtitle text" />
532
+ <List>
533
+ <ListItem title="First Item" subtitle="Subtitle text" />
534
+ <ListItem title="Second Item" subtitle="Another subtitle" />
535
+ <ListItem title="Third Item" subtitle="One more" />
536
+ </List>
529
537
  </ThemeProvider>
530
538
  </td>
531
539
  </tr>
532
540
  <tr>
533
- <td class="variation-label">ListItem - With Icons</td>
541
+ <td class="variation-label">List + ListItem - With Icons</td>
534
542
  <td>
535
543
  <ThemeProvider domain="domain-dental">
536
- <ListItem title="Item with Icons" subtitle="Prepend and append icons" prepend-icon="mdi-account" append-icon="mdi-chevron-right" />
544
+ <List density="compact">
545
+ <ListItem title="Kalender" prepend-icon="mdi-calendar" append-icon="mdi-chevron-right" />
546
+ <ListItem title="Patienten" prepend-icon="mdi-account-group" append-icon="mdi-chevron-right" />
547
+ <ListItem title="Einstellungen" prepend-icon="mdi-cog" append-icon="mdi-chevron-right" />
548
+ </List>
537
549
  </ThemeProvider>
538
550
  </td>
539
551
  <td>
540
552
  <ThemeProvider domain="domain-cocoon">
541
- <ListItem title="Item with Icons" subtitle="Prepend and append icons" prepend-icon="mdi-account" append-icon="mdi-chevron-right" />
553
+ <List density="compact">
554
+ <ListItem title="Kalender" prepend-icon="mdi-calendar" append-icon="mdi-chevron-right" />
555
+ <ListItem title="Patienten" prepend-icon="mdi-account-group" append-icon="mdi-chevron-right" />
556
+ <ListItem title="Einstellungen" prepend-icon="mdi-cog" append-icon="mdi-chevron-right" />
557
+ </List>
542
558
  </ThemeProvider>
543
559
  </td>
544
560
  </tr>
545
561
  <tr>
546
- <td class="variation-label">ListItem - Disabled</td>
562
+ <td class="variation-label">List + ListItem - Nav</td>
547
563
  <td>
548
564
  <ThemeProvider domain="domain-dental">
549
- <ListItem title="Disabled Item" subtitle="Cannot interact" :disabled="true" />
565
+ <List density="compact" :nav="true">
566
+ <ListItem title="Dashboard" prepend-icon="mdi-view-dashboard" :active="true" color="primary" rounded="lg" />
567
+ <ListItem title="Termine" prepend-icon="mdi-calendar-check" rounded="lg" />
568
+ <ListItem title="Nachrichten" prepend-icon="mdi-message-text" rounded="lg" />
569
+ </List>
550
570
  </ThemeProvider>
551
571
  </td>
552
572
  <td>
553
573
  <ThemeProvider domain="domain-cocoon">
554
- <ListItem title="Disabled Item" subtitle="Cannot interact" :disabled="true" />
574
+ <List density="compact" :nav="true">
575
+ <ListItem title="Dashboard" prepend-icon="mdi-view-dashboard" :active="true" color="primary" rounded="lg" />
576
+ <ListItem title="Termine" prepend-icon="mdi-calendar-check" rounded="lg" />
577
+ <ListItem title="Nachrichten" prepend-icon="mdi-message-text" rounded="lg" />
578
+ </List>
555
579
  </ThemeProvider>
556
580
  </td>
557
581
  </tr>
558
582
  <tr>
559
- <td class="variation-label">ListItem - Custom Slot</td>
583
+ <td class="variation-label">List + ListItem - Disabled</td>
560
584
  <td>
561
585
  <ThemeProvider domain="domain-dental">
562
- <ListItem :title="undefined">
563
- <template #default>
564
- <div style="display: flex; justify-content: space-between; width: 100%;">
565
- <div>
566
- <div style="font-weight: 500;">Weber, Wolfgang</div>
567
- <div style="font-size: 0.75rem; color: #9e9e9e;">#06432</div>
568
- </div>
569
- <span>12.09.2001</span>
570
- </div>
571
- </template>
572
- </ListItem>
586
+ <List :disabled="true">
587
+ <ListItem title="All items disabled" subtitle="Via List prop" prepend-icon="mdi-cancel" />
588
+ <ListItem title="Second disabled" subtitle="Inherited from parent" prepend-icon="mdi-cancel" />
589
+ </List>
573
590
  </ThemeProvider>
574
591
  </td>
575
592
  <td>
576
593
  <ThemeProvider domain="domain-cocoon">
577
- <ListItem :title="undefined">
578
- <template #default>
579
- <div style="display: flex; justify-content: space-between; width: 100%;">
580
- <div>
581
- <div style="font-weight: 500;">Weber, Wolfgang</div>
582
- <div style="font-size: 0.75rem; color: #9e9e9e;">#06432</div>
583
- </div>
584
- <span>12.09.2001</span>
585
- </div>
586
- </template>
587
- </ListItem>
594
+ <List :disabled="true">
595
+ <ListItem title="All items disabled" subtitle="Via List prop" prepend-icon="mdi-cancel" />
596
+ <ListItem title="Second disabled" subtitle="Inherited from parent" prepend-icon="mdi-cancel" />
597
+ </List>
588
598
  </ThemeProvider>
589
599
  </td>
590
600
  </tr>
591
601
  <tr>
592
- <td class="variation-label">ListItem - Active</td>
602
+ <td class="variation-label">List + ListItem - Custom Slot</td>
593
603
  <td>
594
604
  <ThemeProvider domain="domain-dental">
595
- <ListItem title="Active Item" subtitle="Currently selected" :active="true" color="primary" />
605
+ <List>
606
+ <ListItem :title="undefined">
607
+ <template #default>
608
+ <div style="display: flex; justify-content: space-between; width: 100%;">
609
+ <div>
610
+ <div style="font-weight: 500;">Weber, Wolfgang</div>
611
+ <div style="font-size: 0.75rem; color: #9e9e9e;">#06432</div>
612
+ </div>
613
+ <span>12.09.2001</span>
614
+ </div>
615
+ </template>
616
+ </ListItem>
617
+ </List>
596
618
  </ThemeProvider>
597
619
  </td>
598
620
  <td>
599
621
  <ThemeProvider domain="domain-cocoon">
600
- <ListItem title="Active Item" subtitle="Currently selected" :active="true" color="primary" />
622
+ <List>
623
+ <ListItem :title="undefined">
624
+ <template #default>
625
+ <div style="display: flex; justify-content: space-between; width: 100%;">
626
+ <div>
627
+ <div style="font-weight: 500;">Weber, Wolfgang</div>
628
+ <div style="font-size: 0.75rem; color: #9e9e9e;">#06432</div>
629
+ </div>
630
+ <span>12.09.2001</span>
631
+ </div>
632
+ </template>
633
+ </ListItem>
634
+ </List>
601
635
  </ThemeProvider>
602
636
  </td>
603
637
  </tr>
@@ -1855,6 +1889,7 @@
1855
1889
  import PhoneInput from '@/components/PhoneInput/PhoneInput.vue';
1856
1890
  import Select from '@/components/Select/Select.vue';
1857
1891
  import SelectAutocomplete from '@/components/SelectAutocomplete/SelectAutocomplete.vue';
1892
+ import List from '@/components/List/List.vue';
1858
1893
  import ListItem from '@/components/ListItem/ListItem.vue';
1859
1894
  import TextArea from '@/components/TextArea/TextArea.vue';
1860
1895
  import TickBox from '@/components/TickBox/TickBox.vue';
@@ -0,0 +1,511 @@
1
+ <template>
2
+ <div class="page">
3
+ <div class="element-container">
4
+ <div class="controls">
5
+ <h2>Testing Domain: {{ currentDomain }}</h2>
6
+ <div class="button-group">
7
+ <Button label="Switch to Wunschlachen (Dental)" @click="switchToDental" variant="flat" color="#4caf50" text-color="#ffffff" />
8
+ <Button label="Switch to White Cocoon" @click="switchToCocoon" variant="flat" color="#4caf50" text-color="#ffffff" />
9
+ <Button label="Reset to Auto-Detect" @click="resetDomain" variant="flat" color="#f44336" text-color="#ffffff" />
10
+ </div>
11
+ </div>
12
+
13
+ <!-- Basic ListItem -->
14
+ <div class="section">
15
+ <h3 class="section-title">Basic ListItem</h3>
16
+ <div class="examples-grid">
17
+ <div class="example-item">
18
+ <h4>Title Only</h4>
19
+ <List class="demo-list">
20
+ <ListItem title="Zahnreinigung" />
21
+ <ListItem title="Kontrolluntersuchung" />
22
+ <ListItem title="Implantologie" />
23
+ </List>
24
+ </div>
25
+
26
+ <div class="example-item">
27
+ <h4>Title + Subtitle</h4>
28
+ <List class="demo-list">
29
+ <ListItem title="Zahnreinigung" subtitle="Professionelle Reinigung und Politur" />
30
+ <ListItem title="Kontrolluntersuchung" subtitle="Regelmäßige Kontrolle der Zahngesundheit" />
31
+ <ListItem title="Implantologie" subtitle="Ersatz fehlender Zähne durch Implantate" />
32
+ </List>
33
+ </div>
34
+
35
+ <div class="example-item">
36
+ <h4>With Icons</h4>
37
+ <List class="demo-list">
38
+ <ListItem title="Dr. Maria Schmidt" subtitle="Zahnärztin" prepend-icon="mdi-account" append-icon="mdi-chevron-right" />
39
+ <ListItem title="Dr. Thomas Bauer" subtitle="Kieferorthopäde" prepend-icon="mdi-account" append-icon="mdi-chevron-right" />
40
+ <ListItem title="Julia Fischer" subtitle="Assistenz" prepend-icon="mdi-account" append-icon="mdi-chevron-right" />
41
+ </List>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- States -->
47
+ <div class="section">
48
+ <h3 class="section-title">States</h3>
49
+ <div class="examples-grid">
50
+ <div class="example-item">
51
+ <h4>Normal</h4>
52
+ <List class="demo-list">
53
+ <ListItem title="Normal Item" subtitle="Default state" prepend-icon="mdi-circle-outline" />
54
+ </List>
55
+ </div>
56
+
57
+ <div class="example-item">
58
+ <h4>Active</h4>
59
+ <List class="demo-list">
60
+ <ListItem title="Active Item" subtitle="Currently selected" :active="true" color="primary" prepend-icon="mdi-check-circle" />
61
+ </List>
62
+ </div>
63
+
64
+ <div class="example-item">
65
+ <h4>Disabled</h4>
66
+ <List class="demo-list">
67
+ <ListItem title="Disabled Item" subtitle="Cannot interact" :disabled="true" prepend-icon="mdi-cancel" />
68
+ </List>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Density -->
74
+ <div class="section">
75
+ <h3 class="section-title">Density</h3>
76
+ <div class="examples-grid">
77
+ <div class="example-item">
78
+ <h4>Default</h4>
79
+ <List class="demo-list">
80
+ <ListItem title="Default density" subtitle="More vertical space" density="default" prepend-icon="mdi-square-outline" />
81
+ </List>
82
+ </div>
83
+
84
+ <div class="example-item">
85
+ <h4>Comfortable</h4>
86
+ <List class="demo-list">
87
+ <ListItem title="Comfortable density" subtitle="Medium spacing" density="comfortable" prepend-icon="mdi-square-outline" />
88
+ </List>
89
+ </div>
90
+
91
+ <div class="example-item">
92
+ <h4>Compact</h4>
93
+ <List class="demo-list">
94
+ <ListItem title="Compact density" subtitle="Minimal spacing" density="compact" prepend-icon="mdi-square-outline" />
95
+ </List>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Practical: Selectable List -->
101
+ <div class="section">
102
+ <h3 class="section-title">Selectable List</h3>
103
+ <p class="section-description">Click to select a treatment</p>
104
+ <div class="examples-grid">
105
+ <div class="example-item">
106
+ <List class="demo-list">
107
+ <ListItem
108
+ v-for="treatment in treatments"
109
+ :key="treatment.value"
110
+ :title="treatment.title"
111
+ :subtitle="treatment.subtitle"
112
+ :active="selectedTreatment === treatment.value"
113
+ :value="treatment.value"
114
+ color="primary"
115
+ prepend-icon="mdi-medical-bag"
116
+ :append-icon="selectedTreatment === treatment.value ? 'mdi-check' : undefined"
117
+ @click="selectedTreatment = treatment.value"
118
+ />
119
+ </List>
120
+ <p class="value-display">Selected: {{ selectedTreatment || 'None' }}</p>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Practical: Navigation Menu -->
126
+ <div class="section">
127
+ <h3 class="section-title">Navigation Menu</h3>
128
+ <p class="section-description">Sidebar-style navigation with icons</p>
129
+ <div class="examples-grid">
130
+ <div class="example-item">
131
+ <List class="demo-list" density="compact" nav>
132
+ <ListItem
133
+ v-for="nav in navItems"
134
+ :key="nav.value"
135
+ :title="nav.title"
136
+ :prepend-icon="nav.icon"
137
+ :active="activeNav === nav.value"
138
+ :value="nav.value"
139
+ color="primary"
140
+ rounded="lg"
141
+ @click="activeNav = nav.value"
142
+ />
143
+ </List>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <!-- Practical: Custom Slot Content -->
149
+ <div class="section">
150
+ <h3 class="section-title">Custom Slot Content</h3>
151
+ <p class="section-description">Patient list, appointment list, and notification list using custom slots</p>
152
+ <div class="examples-grid">
153
+ <div class="example-item">
154
+ <h4>Patient List</h4>
155
+ <List class="demo-list">
156
+ <ListItem
157
+ v-for="patient in patients"
158
+ :key="patient.id"
159
+ :title="undefined"
160
+ @click="selectedPatient = patient.id"
161
+ >
162
+ <template #prepend>
163
+ <div class="patient-avatar">{{ patient.initials }}</div>
164
+ </template>
165
+ <template #default>
166
+ <div class="patient-info">
167
+ <span class="patient-name">{{ patient.name }}</span>
168
+ <span class="patient-meta">#{{ patient.number }} &middot; {{ patient.dob }}</span>
169
+ </div>
170
+ </template>
171
+ <template #append>
172
+ <span class="status-badge" :class="patient.status">{{ patient.statusLabel }}</span>
173
+ </template>
174
+ </ListItem>
175
+ </List>
176
+ </div>
177
+
178
+ <div class="example-item">
179
+ <h4>Appointment List</h4>
180
+ <List class="demo-list">
181
+ <ListItem
182
+ v-for="appt in appointments"
183
+ :key="appt.id"
184
+ :title="undefined"
185
+ >
186
+ <template #prepend>
187
+ <div class="appt-time">
188
+ <span class="appt-hour">{{ appt.time }}</span>
189
+ <span class="appt-duration">{{ appt.duration }}</span>
190
+ </div>
191
+ </template>
192
+ <template #default>
193
+ <div class="appt-info">
194
+ <span class="appt-patient">{{ appt.patient }}</span>
195
+ <span class="appt-treatment">{{ appt.treatment }}</span>
196
+ </div>
197
+ </template>
198
+ <template #append>
199
+ <span class="appt-dentist">{{ appt.dentist }}</span>
200
+ </template>
201
+ </ListItem>
202
+ </List>
203
+ </div>
204
+
205
+ <div class="example-item">
206
+ <h4>Notification List</h4>
207
+ <List class="demo-list">
208
+ <ListItem
209
+ v-for="notif in notifications"
210
+ :key="notif.id"
211
+ :title="undefined"
212
+ >
213
+ <template #prepend>
214
+ <v-icon :color="notif.color">{{ notif.icon }}</v-icon>
215
+ </template>
216
+ <template #default>
217
+ <div class="notif-info">
218
+ <span class="notif-title">{{ notif.title }}</span>
219
+ <span class="notif-desc">{{ notif.desc }}</span>
220
+ </div>
221
+ </template>
222
+ <template #append>
223
+ <span class="notif-time">{{ notif.time }}</span>
224
+ </template>
225
+ </ListItem>
226
+ </List>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </template>
233
+
234
+ <script setup lang="ts">
235
+ import { computed, ref } from "vue";
236
+ import {
237
+ domain,
238
+ setTestingDomain,
239
+ clearTestingDomain,
240
+ } from "@/utils/index";
241
+
242
+ import Button from "@/components/Button/Button.vue";
243
+ import List from "@/components/List/List.vue";
244
+ import ListItem from "@/components/ListItem/ListItem.vue";
245
+
246
+ const currentDomain = computed(() => domain.value);
247
+ const switchToDental = () => setTestingDomain("domain-dental");
248
+ const switchToCocoon = () => setTestingDomain("domain-cocoon");
249
+ const resetDomain = () => clearTestingDomain();
250
+
251
+ // Selectable list
252
+ const selectedTreatment = ref<string | null>(null);
253
+ const treatments = [
254
+ { title: 'Zahnreinigung', subtitle: 'Professionelle Reinigung und Politur', value: 'cleaning' },
255
+ { title: 'Kontrolluntersuchung', subtitle: 'Regelmäßige Kontrolle', value: 'checkup' },
256
+ { title: 'Implantologie', subtitle: 'Ersatz fehlender Zähne', value: 'implant' },
257
+ { title: 'Wurzelbehandlung', subtitle: 'Behandlung des Zahnmarks', value: 'root' },
258
+ { title: 'Kieferorthopädie', subtitle: 'Korrektur von Zahnfehlstellungen', value: 'ortho' },
259
+ ];
260
+
261
+ // Navigation menu
262
+ const activeNav = ref('calendar');
263
+ const navItems = [
264
+ { title: 'Kalender', icon: 'mdi-calendar', value: 'calendar' },
265
+ { title: 'Patienten', icon: 'mdi-account-group', value: 'patients' },
266
+ { title: 'Behandlungen', icon: 'mdi-medical-bag', value: 'treatments' },
267
+ { title: 'Nachrichten', icon: 'mdi-message-text', value: 'messages' },
268
+ { title: 'Einstellungen', icon: 'mdi-cog', value: 'settings' },
269
+ ];
270
+
271
+ // Patient list
272
+ const selectedPatient = ref<string | null>(null);
273
+ const patients = [
274
+ { id: '1', name: 'Weber, Wolfgang', number: '06432', dob: '12.09.2001', initials: 'WW', status: 'active', statusLabel: 'Aktiv' },
275
+ { id: '2', name: 'Krause, Wolfram', number: '26152', dob: '12.04.1954', initials: 'KW', status: 'waiting', statusLabel: 'Wartend' },
276
+ { id: '3', name: 'Adler, Wolfbert', number: '83827', dob: '12.09.2000', initials: 'AW', status: 'active', statusLabel: 'Aktiv' },
277
+ { id: '4', name: 'Schulz, Wolfgang', number: '72376', dob: '05.12.1987', initials: 'SW', status: 'inactive', statusLabel: 'Inaktiv' },
278
+ ];
279
+
280
+ // Appointment list
281
+ const appointments = [
282
+ { id: '1', time: '09:00', duration: '30 min', patient: 'Weber, Wolfgang', treatment: 'Zahnreinigung', dentist: 'Dr. Schmidt' },
283
+ { id: '2', time: '09:30', duration: '45 min', patient: 'Krause, Wolfram', treatment: 'Kontrolluntersuchung', dentist: 'Dr. Bauer' },
284
+ { id: '3', time: '10:15', duration: '60 min', patient: 'Adler, Wolfbert', treatment: 'Implantologie', dentist: 'Dr. Schmidt' },
285
+ { id: '4', time: '11:15', duration: '30 min', patient: 'Schulz, Wolfgang', treatment: 'Wurzelbehandlung', dentist: 'Dr. Bauer' },
286
+ ];
287
+
288
+ // Notification list
289
+ const notifications = [
290
+ { id: '1', icon: 'mdi-calendar-check', color: '#4caf50', title: 'Termin bestätigt', desc: 'Weber, Wolfgang hat den Termin am 15.02 bestätigt', time: 'vor 5 Min.' },
291
+ { id: '2', icon: 'mdi-calendar-remove', color: '#f44336', title: 'Termin abgesagt', desc: 'Krause, Wolfram hat den Termin am 16.02 abgesagt', time: 'vor 15 Min.' },
292
+ { id: '3', icon: 'mdi-account-plus', color: '#2196f3', title: 'Neuer Patient', desc: 'Müller, Anna wurde als neuer Patient registriert', time: 'vor 1 Std.' },
293
+ { id: '4', icon: 'mdi-alert-circle', color: '#ff9800', title: 'Versicherung ausstehend', desc: 'Schulz, Wolfgang — Versicherungsstatus prüfen', time: 'vor 2 Std.' },
294
+ ];
295
+ </script>
296
+
297
+ <style scoped>
298
+ .page {
299
+ display: flex;
300
+ flex-direction: column;
301
+ justify-content: flex-start;
302
+ align-items: center;
303
+ width: 100vw;
304
+ min-height: 100vh;
305
+ background-color: #f5f5f5;
306
+ padding: 2rem 0;
307
+ overflow-y: auto;
308
+ }
309
+
310
+ .element-container {
311
+ display: flex;
312
+ flex-direction: column;
313
+ justify-content: flex-start;
314
+ align-items: center;
315
+ width: 95%;
316
+ max-width: 1400px;
317
+ gap: 2rem;
318
+ }
319
+
320
+ .controls {
321
+ width: 100%;
322
+ background: white;
323
+ padding: 1.5rem;
324
+ border-radius: 8px;
325
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
326
+ text-align: center;
327
+ }
328
+
329
+ .controls h2 {
330
+ margin: 0 0 1rem 0;
331
+ color: #333;
332
+ font-size: 1.5rem;
333
+ }
334
+
335
+ .button-group {
336
+ display: flex;
337
+ gap: 1rem;
338
+ justify-content: center;
339
+ flex-wrap: wrap;
340
+ }
341
+
342
+
343
+ .section {
344
+ width: 100%;
345
+ background: white;
346
+ padding: 1.5rem;
347
+ border-radius: 8px;
348
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
349
+ }
350
+
351
+ .section-title {
352
+ margin: 0 0 1.5rem 0;
353
+ color: #333;
354
+ font-size: 1.25rem;
355
+ font-weight: 600;
356
+ border-bottom: 2px solid #e0e0e0;
357
+ padding-bottom: 0.5rem;
358
+ }
359
+
360
+ .section-description {
361
+ margin: -1rem 0 1.5rem 0;
362
+ font-size: 0.875rem;
363
+ color: #666;
364
+ }
365
+
366
+ .examples-grid {
367
+ display: grid;
368
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
369
+ gap: 1.5rem;
370
+ }
371
+
372
+ .example-item {
373
+ display: flex;
374
+ flex-direction: column;
375
+ gap: 0.5rem;
376
+ }
377
+
378
+ .example-item h4 {
379
+ margin: 0;
380
+ font-size: 0.875rem;
381
+ font-weight: 600;
382
+ color: #555;
383
+ }
384
+
385
+ .value-display {
386
+ margin: 0.5rem 0 0 0;
387
+ font-size: 0.8125rem;
388
+ color: #666;
389
+ font-style: italic;
390
+ }
391
+
392
+ .demo-list {
393
+ border: 1px solid #e0e0e0;
394
+ border-radius: 8px;
395
+ overflow: hidden;
396
+ }
397
+
398
+ /* Patient list styles */
399
+ .patient-avatar {
400
+ width: 36px;
401
+ height: 36px;
402
+ border-radius: 50%;
403
+ background-color: #172774;
404
+ color: white;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: center;
408
+ font-size: 0.75rem;
409
+ font-weight: 600;
410
+ margin-right: 0.75rem;
411
+ }
412
+
413
+ .patient-info {
414
+ display: flex;
415
+ flex-direction: column;
416
+ }
417
+
418
+ .patient-name {
419
+ font-weight: 500;
420
+ color: #172774;
421
+ font-size: 0.9375rem;
422
+ }
423
+
424
+ .patient-meta {
425
+ font-size: 0.75rem;
426
+ color: #9e9e9e;
427
+ }
428
+
429
+ .status-badge {
430
+ font-size: 0.7rem;
431
+ padding: 0.125rem 0.5rem;
432
+ border-radius: 999px;
433
+ font-weight: 500;
434
+ }
435
+
436
+ .status-badge.active { background-color: #e8f5e9; color: #2e7d32; }
437
+ .status-badge.waiting { background-color: #fff3e0; color: #ef6c00; }
438
+ .status-badge.inactive { background-color: #f5f5f5; color: #757575; }
439
+
440
+ /* Appointment list styles */
441
+ .appt-time {
442
+ display: flex;
443
+ flex-direction: column;
444
+ align-items: center;
445
+ min-width: 50px;
446
+ margin-right: 0.75rem;
447
+ }
448
+
449
+ .appt-hour {
450
+ font-weight: 600;
451
+ color: #172774;
452
+ font-size: 0.9375rem;
453
+ }
454
+
455
+ .appt-duration {
456
+ font-size: 0.6875rem;
457
+ color: #9e9e9e;
458
+ }
459
+
460
+ .appt-info {
461
+ display: flex;
462
+ flex-direction: column;
463
+ }
464
+
465
+ .appt-patient {
466
+ font-weight: 500;
467
+ color: #172774;
468
+ font-size: 0.9375rem;
469
+ }
470
+
471
+ .appt-treatment {
472
+ font-size: 0.75rem;
473
+ color: #9e9e9e;
474
+ }
475
+
476
+ .appt-dentist {
477
+ font-size: 0.75rem;
478
+ color: #666;
479
+ white-space: nowrap;
480
+ }
481
+
482
+ /* Notification list styles */
483
+ .notif-info {
484
+ display: flex;
485
+ flex-direction: column;
486
+ }
487
+
488
+ .notif-title {
489
+ font-weight: 500;
490
+ color: #172774;
491
+ font-size: 0.9375rem;
492
+ }
493
+
494
+ .notif-desc {
495
+ font-size: 0.75rem;
496
+ color: #9e9e9e;
497
+ line-height: 1.3;
498
+ }
499
+
500
+ .notif-time {
501
+ font-size: 0.6875rem;
502
+ color: #9e9e9e;
503
+ white-space: nowrap;
504
+ }
505
+
506
+ @media (max-width: 768px) {
507
+ .examples-grid {
508
+ grid-template-columns: 1fr;
509
+ }
510
+ }
511
+ </style>
@@ -589,7 +589,7 @@
589
589
  <svg v-if="!patientSearchQuery" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
590
590
  </template>
591
591
  <template #item="{ item, props: itemProps }">
592
- <ListItem v-bind="itemProps" :title="undefined" class="patient-search-item">
592
+ <v-list-item v-bind="itemProps" :title="undefined" class="patient-search-item">
593
593
  <template #default>
594
594
  <div class="patient-row">
595
595
  <div class="patient-left">
@@ -599,7 +599,7 @@
599
599
  <span class="patient-dob">{{ item.raw.dob }}</span>
600
600
  </div>
601
601
  </template>
602
- </ListItem>
602
+ </v-list-item>
603
603
  </template>
604
604
  <template #no-data>
605
605
  <div class="patient-no-data">