@birdycom/omniaui-vue 0.11.0 → 0.11.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.
Files changed (178) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +49 -49
  3. package/dist/omniaui-vue.js.map +1 -1
  4. package/package.json +69 -69
  5. package/src/blocks/OcBlockAboutTimeline.vue +44 -44
  6. package/src/blocks/OcBlockAppHeader.vue +36 -36
  7. package/src/blocks/OcBlockAppShell.vue +29 -29
  8. package/src/blocks/OcBlockAuth.vue +24 -24
  9. package/src/blocks/OcBlockAuthSplit.vue +47 -47
  10. package/src/blocks/OcBlockContactGrid.vue +53 -53
  11. package/src/blocks/OcBlockCta.vue +12 -12
  12. package/src/blocks/OcBlockDataTable.vue +27 -27
  13. package/src/blocks/OcBlockFaq.vue +49 -49
  14. package/src/blocks/OcBlockFeatures.vue +23 -23
  15. package/src/blocks/OcBlockFooter.vue +25 -25
  16. package/src/blocks/OcBlockFormPage.vue +24 -24
  17. package/src/blocks/OcBlockHero.vue +13 -13
  18. package/src/blocks/OcBlockHeroCode.vue +42 -42
  19. package/src/blocks/OcBlockHeroOrbit.vue +69 -69
  20. package/src/blocks/OcBlockHeroSplit.vue +15 -15
  21. package/src/blocks/OcBlockModuleHeader.vue +21 -21
  22. package/src/blocks/OcBlockPageHero.vue +13 -13
  23. package/src/blocks/OcBlockPortfolioGrid.vue +63 -63
  24. package/src/blocks/OcBlockPricing.vue +39 -39
  25. package/src/blocks/OcBlockProcess.vue +39 -39
  26. package/src/blocks/OcBlockProductGrid.vue +41 -41
  27. package/src/blocks/OcBlockProfileHeader.vue +50 -50
  28. package/src/blocks/OcBlockShowcase.vue +43 -43
  29. package/src/blocks/OcBlockSiteFooter.vue +54 -54
  30. package/src/blocks/OcBlockSiteHeader.vue +34 -34
  31. package/src/blocks/OcBlockSpotlight.vue +47 -47
  32. package/src/blocks/OcBlockStats.vue +17 -17
  33. package/src/blocks/OcBlockStatsCounter.vue +20 -20
  34. package/src/blocks/OcBlockTeamList.vue +53 -53
  35. package/src/blocks/OcBlockTwoFactor.vue +61 -61
  36. package/src/blocks/OcBlockWizardDialog.vue +67 -67
  37. package/src/components/OcAccordion.vue +12 -12
  38. package/src/components/OcAccordionAccent.vue +27 -27
  39. package/src/components/OcAccordionItem.vue +19 -19
  40. package/src/components/OcAffixInput.vue +40 -40
  41. package/src/components/OcAlert.vue +17 -17
  42. package/src/components/OcAlertDialog.vue +66 -66
  43. package/src/components/OcAlphaTabs.vue +54 -54
  44. package/src/components/OcAspectRatio.vue +21 -21
  45. package/src/components/OcAttachment.vue +59 -59
  46. package/src/components/OcAttachmentImage.vue +13 -13
  47. package/src/components/OcAttachmentList.vue +31 -31
  48. package/src/components/OcAttachmentThumb.vue +14 -14
  49. package/src/components/OcAvatar.vue +18 -18
  50. package/src/components/OcAvatarGroup.vue +7 -7
  51. package/src/components/OcBackLink.vue +8 -8
  52. package/src/components/OcBadge.vue +13 -13
  53. package/src/components/OcBarcodeInput.vue +51 -51
  54. package/src/components/OcBreadcrumb.vue +22 -22
  55. package/src/components/OcBrowserFrame.vue +33 -33
  56. package/src/components/OcBusyOverlay.vue +32 -32
  57. package/src/components/OcButton.vue +47 -47
  58. package/src/components/OcButtonGroup.vue +10 -10
  59. package/src/components/OcButtonGroupText.vue +7 -7
  60. package/src/components/OcCard.vue +20 -20
  61. package/src/components/OcChartLine.vue +80 -80
  62. package/src/components/OcChatComposer.vue +42 -42
  63. package/src/components/OcCheckbox.vue +26 -26
  64. package/src/components/OcChecklist.vue +29 -29
  65. package/src/components/OcCollapsible.vue +19 -19
  66. package/src/components/OcColorField.vue +15 -15
  67. package/src/components/OcColorSelect.vue +33 -33
  68. package/src/components/OcColumnMenu.vue +50 -50
  69. package/src/components/OcCommand.vue +119 -119
  70. package/src/components/OcConfirmDialog.vue +65 -65
  71. package/src/components/OcCookieConsent.vue +119 -119
  72. package/src/components/OcCopyField.vue +37 -37
  73. package/src/components/OcCounter.vue +15 -15
  74. package/src/components/OcDataTable.vue +144 -144
  75. package/src/components/OcDefinitionList.vue +35 -35
  76. package/src/components/OcDropdown.vue +23 -23
  77. package/src/components/OcDropdownItem.vue +9 -9
  78. package/src/components/OcDropdownLabel.vue +7 -7
  79. package/src/components/OcDropdownSeparator.vue +7 -7
  80. package/src/components/OcDropzone.vue +36 -36
  81. package/src/components/OcEmojiPicker.vue +45 -45
  82. package/src/components/OcEmptyState.vue +13 -13
  83. package/src/components/OcEntityCard.vue +47 -47
  84. package/src/components/OcFactList.vue +37 -37
  85. package/src/components/OcFilterCounts.vue +38 -38
  86. package/src/components/OcFlipCard.vue +36 -36
  87. package/src/components/OcFormField.vue +36 -36
  88. package/src/components/OcGreeting.vue +52 -52
  89. package/src/components/OcHeading.vue +19 -19
  90. package/src/components/OcHoverCard.vue +25 -25
  91. package/src/components/OcImageZoom.vue +39 -39
  92. package/src/components/OcInput.vue +9 -9
  93. package/src/components/OcInputGroup.vue +12 -12
  94. package/src/components/OcItem.vue +40 -40
  95. package/src/components/OcItemGroup.vue +7 -7
  96. package/src/components/OcItemSeparator.vue +7 -7
  97. package/src/components/OcKbd.vue +7 -7
  98. package/src/components/OcKbdGroup.vue +15 -15
  99. package/src/components/OcLabel.vue +10 -10
  100. package/src/components/OcLanguageSelect.vue +40 -40
  101. package/src/components/OcLauncherCard.vue +34 -34
  102. package/src/components/OcLightbox.vue +63 -63
  103. package/src/components/OcLinkArrow.vue +25 -25
  104. package/src/components/OcListToolbar.vue +44 -44
  105. package/src/components/OcMarquee.vue +36 -36
  106. package/src/components/OcMediaCard.vue +28 -28
  107. package/src/components/OcMediaRow.vue +28 -28
  108. package/src/components/OcMediaThumb.vue +38 -38
  109. package/src/components/OcMessage.vue +91 -91
  110. package/src/components/OcMessageList.vue +43 -43
  111. package/src/components/OcMessageThread.vue +155 -155
  112. package/src/components/OcModal.vue +42 -42
  113. package/src/components/OcNavmenu.vue +18 -18
  114. package/src/components/OcNavmenuList.vue +38 -38
  115. package/src/components/OcNextTeaser.vue +25 -25
  116. package/src/components/OcNumberInput.vue +47 -47
  117. package/src/components/OcOtpInput.vue +34 -34
  118. package/src/components/OcPageHeader.vue +34 -34
  119. package/src/components/OcPagination.vue +33 -33
  120. package/src/components/OcPasswordInput.vue +27 -27
  121. package/src/components/OcPasswordRequirements.vue +28 -28
  122. package/src/components/OcPhoneInput.vue +72 -72
  123. package/src/components/OcPopover.vue +25 -25
  124. package/src/components/OcPriceCard.vue +69 -69
  125. package/src/components/OcProductMark.vue +31 -31
  126. package/src/components/OcProgress.vue +21 -21
  127. package/src/components/OcProjectCard.vue +36 -36
  128. package/src/components/OcProse.vue +57 -57
  129. package/src/components/OcRadio.vue +40 -40
  130. package/src/components/OcRadioGroup.vue +32 -32
  131. package/src/components/OcRecoveryCodes.vue +55 -55
  132. package/src/components/OcScrollArea.vue +15 -15
  133. package/src/components/OcSegmented.vue +28 -28
  134. package/src/components/OcSelect.vue +21 -21
  135. package/src/components/OcSeparator.vue +18 -18
  136. package/src/components/OcSeverityCard.vue +46 -46
  137. package/src/components/OcSheet.vue +49 -49
  138. package/src/components/OcSidebarCollapsible.vue +19 -19
  139. package/src/components/OcSidebarGroup.vue +11 -11
  140. package/src/components/OcSidebarMenu.vue +7 -7
  141. package/src/components/OcSidebarMenuButton.vue +31 -31
  142. package/src/components/OcSidebarSeparator.vue +7 -7
  143. package/src/components/OcSkeleton.vue +8 -8
  144. package/src/components/OcSlider.vue +27 -27
  145. package/src/components/OcSortableRow.vue +40 -40
  146. package/src/components/OcSpinner.vue +13 -13
  147. package/src/components/OcStackCard.vue +24 -24
  148. package/src/components/OcStackList.vue +31 -31
  149. package/src/components/OcStatCard.vue +39 -39
  150. package/src/components/OcStepper.vue +34 -34
  151. package/src/components/OcSwitch.vue +15 -15
  152. package/src/components/OcTable.vue +115 -115
  153. package/src/components/OcTableActions.vue +26 -26
  154. package/src/components/OcTableCompact.vue +46 -46
  155. package/src/components/OcTabs.vue +37 -37
  156. package/src/components/OcTagInput.vue +50 -50
  157. package/src/components/OcTerminalLog.vue +50 -50
  158. package/src/components/OcTextarea.vue +9 -9
  159. package/src/components/OcThemeSwitch.vue +31 -31
  160. package/src/components/OcTimeInput.vue +50 -50
  161. package/src/components/OcTimeline.vue +36 -36
  162. package/src/components/OcToast.vue +31 -31
  163. package/src/components/OcToastContainer.vue +21 -21
  164. package/src/components/OcToggle.vue +63 -63
  165. package/src/components/OcToggleField.vue +28 -28
  166. package/src/components/OcToggleGroup.vue +40 -40
  167. package/src/components/OcTooltip.vue +8 -8
  168. package/src/components/OcTopProgress.vue +54 -54
  169. package/src/components/OcTree.vue +70 -70
  170. package/src/components/OcUpgradeBanner.vue +42 -42
  171. package/src/components/OcUser.vue +25 -25
  172. package/src/components/OcVersionBanner.vue +34 -34
  173. package/src/composables/chartLine.ts +28 -28
  174. package/src/composables/keys.ts +22 -22
  175. package/src/composables/useAttrsSplit.ts +16 -16
  176. package/src/composables/useDialog.ts +21 -21
  177. package/src/composables/useToast.ts +66 -66
  178. package/src/composables/useUid.ts +12 -12
@@ -1,43 +1,43 @@
1
- <!-- File: packages/vue/src/components/OcMessageList.vue | Map: props + demo conversation -> .oc-message-list -> dividers and OcMessage rows -->
2
- <script setup lang="ts">
3
- /**
4
- * Column of messages and dividers (snippets/message.html). items: (MessageData | { divider, system? })[]; demo conversation
5
- * when empty; slot default replaces them. log = role="log" (default true). Emits: action(name, message, index).
6
- * Classes: .oc-message-list, .oc-message-divider(--system)
7
- */
8
- import OcMessage, { type MessageData } from './OcMessage.vue'
9
-
10
- export type MessageListItem = MessageData | { divider: string; system?: boolean }
11
- withDefaults(defineProps<{ items?: MessageListItem[]; log?: boolean; label?: string }>(), {
12
- log: true,
13
- label: 'Conversation with Lisa Verbeke',
14
- items: () => [
15
- { divider: 'Tuesday 14 October' },
16
- { sender: 'Lisa Verbeke', avatar: 'LV', time: '09:12', datetime: '2026-10-14T09:12', label: 'Lisa Verbeke, 09:12', text: 'Morning Tom! The worktop supplier sent two samples for Villa Dewitte.' },
17
- {
18
- sender: 'Lisa Verbeke', avatar: 'LV', time: '09:13', datetime: '2026-10-14T09:13', label: 'Lisa Verbeke, 09:13', continued: true, id: 'oc-message-demo-question',
19
- html: '<p>Which finish should we order?</p><ul><li><strong>Calacatta</strong> matt, 20 mm</li><li><strong>Nero Marquina</strong> polished, 12 mm</li></ul><p>Details are on the <a href="#supplier-sheet">supplier sheet</a>.</p>',
20
- },
21
- {
22
- direction: 'out', avatar: 'TP', time: '09:20', datetime: '2026-10-14T09:20', label: 'You, 09:20', status: 'read', statusIcon: true,
23
- quote: { href: '#oc-message-demo-question', sender: 'Lisa Verbeke', text: 'Which finish should we order? Calacatta matt, 20 mm or Nero Marquina polished, 12 mm' },
24
- text: 'Calacatta matt. The client wants the lighter look next to the oak fronts.',
25
- actions: [{ name: 'reply', label: 'Reply to this message' }, { name: 'delete', label: 'Delete this message', destructive: true }],
26
- },
27
- { divider: 'Lisa Verbeke marked this question as answered', system: true },
28
- ],
29
- })
30
- const emit = defineEmits<{ action: [name: string, message: MessageData, index: number] }>()
31
- const isDivider = (item: MessageListItem): item is { divider: string; system?: boolean } => 'divider' in item
32
- </script>
33
-
34
- <template>
35
- <div class="oc-message-list" :role="log ? 'log' : undefined" :aria-label="log ? label : undefined">
36
- <slot>
37
- <template v-for="(item, i) in items" :key="i">
38
- <div v-if="isDivider(item)" :class="['oc-message-divider', item.system && 'oc-message-divider--system']" :role="item.system ? 'note' : undefined"><span>{{ item.divider }}</span></div>
39
- <OcMessage v-else v-bind="item" @action="(name: string) => emit('action', name, item as MessageData, i)" />
40
- </template>
41
- </slot>
42
- </div>
43
- </template>
1
+ <!-- File: packages/vue/src/components/OcMessageList.vue | Map: props + demo conversation -> .oc-message-list -> dividers and OcMessage rows -->
2
+ <script setup lang="ts">
3
+ /**
4
+ * Column of messages and dividers (snippets/message.html). items: (MessageData | { divider, system? })[]; demo conversation
5
+ * when empty; slot default replaces them. log = role="log" (default true). Emits: action(name, message, index).
6
+ * Classes: .oc-message-list, .oc-message-divider(--system)
7
+ */
8
+ import OcMessage, { type MessageData } from './OcMessage.vue'
9
+
10
+ export type MessageListItem = MessageData | { divider: string; system?: boolean }
11
+ withDefaults(defineProps<{ items?: MessageListItem[]; log?: boolean; label?: string }>(), {
12
+ log: true,
13
+ label: 'Conversation with Lisa Verbeke',
14
+ items: () => [
15
+ { divider: 'Tuesday 14 October' },
16
+ { sender: 'Lisa Verbeke', avatar: 'LV', time: '09:12', datetime: '2026-10-14T09:12', label: 'Lisa Verbeke, 09:12', text: 'Morning Tom! The worktop supplier sent two samples for Villa Dewitte.' },
17
+ {
18
+ sender: 'Lisa Verbeke', avatar: 'LV', time: '09:13', datetime: '2026-10-14T09:13', label: 'Lisa Verbeke, 09:13', continued: true, id: 'oc-message-demo-question',
19
+ html: '<p>Which finish should we order?</p><ul><li><strong>Calacatta</strong> matt, 20 mm</li><li><strong>Nero Marquina</strong> polished, 12 mm</li></ul><p>Details are on the <a href="#supplier-sheet">supplier sheet</a>.</p>',
20
+ },
21
+ {
22
+ direction: 'out', avatar: 'TP', time: '09:20', datetime: '2026-10-14T09:20', label: 'You, 09:20', status: 'read', statusIcon: true,
23
+ quote: { href: '#oc-message-demo-question', sender: 'Lisa Verbeke', text: 'Which finish should we order? Calacatta matt, 20 mm or Nero Marquina polished, 12 mm' },
24
+ text: 'Calacatta matt. The client wants the lighter look next to the oak fronts.',
25
+ actions: [{ name: 'reply', label: 'Reply to this message' }, { name: 'delete', label: 'Delete this message', destructive: true }],
26
+ },
27
+ { divider: 'Lisa Verbeke marked this question as answered', system: true },
28
+ ],
29
+ })
30
+ const emit = defineEmits<{ action: [name: string, message: MessageData, index: number] }>()
31
+ const isDivider = (item: MessageListItem): item is { divider: string; system?: boolean } => 'divider' in item
32
+ </script>
33
+
34
+ <template>
35
+ <div class="oc-message-list" :role="log ? 'log' : undefined" :aria-label="log ? label : undefined">
36
+ <slot>
37
+ <template v-for="(item, i) in items" :key="i">
38
+ <div v-if="isDivider(item)" :class="['oc-message-divider', item.system && 'oc-message-divider--system']" :role="item.system ? 'note' : undefined"><span>{{ item.divider }}</span></div>
39
+ <OcMessage v-else v-bind="item" @action="(name: string) => emit('action', name, item as MessageData, i)" />
40
+ </template>
41
+ </slot>
42
+ </div>
43
+ </template>
@@ -1,155 +1,155 @@
1
- <!-- File: packages/vue/src/components/OcMessageThread.vue | Map: props + demo -> state (messages, sending) -> header (back slot, heading, people, badge, menu)
2
- -> scroll (OcMessageList) -> footer (composer slot / form / note) -> busy overlay -->
3
- <script setup lang="ts">
4
- /**
5
- * Scrolling conversation (same markup as snippets/message-thread.html). Like the snippet script, a sent text is appended as an
6
- * outgoing bubble after the @send handler (prop onSend, may return a Promise) resolves; aria-busy shows the overlay meanwhile.
7
- * Ctrl/Cmd + Enter sends. `manual` = you append messages yourself. Emits: menu(name), tool(name).
8
- * Slots: back (start of the header), composer (replaces the form). Classes: .oc-message-thread(--plain) __header __heading __title
9
- * __meta __people __actions __icon-btn __scroll __footer __note __busy
10
- */
11
- import { ref, watch } from 'vue'
12
- import OcMessageList, { type MessageListItem } from './OcMessageList.vue'
13
- import type { MessageData } from './OcMessage.vue'
14
- import { useUid } from '../composables/useUid'
15
-
16
- export interface ThreadParticipant { initials: string; name?: string }
17
- export type ThreadMenuItem = { name: string; label: string; destructive?: boolean } | { separator: true }
18
-
19
- const props = withDefaults(
20
- defineProps<{
21
- title?: string
22
- project?: string
23
- meta?: string
24
- participants?: ThreadParticipant[]
25
- status?: { label: string; variant?: string } | null
26
- menu?: ThreadMenuItem[] | null
27
- messages?: MessageListItem[]
28
- scrollLabel?: string
29
- live?: boolean
30
- busy?: boolean
31
- /** Overlay title; empty string leaves the overlay out. */
32
- busyTitle?: string
33
- busyText?: string
34
- note?: string
35
- placeholder?: string
36
- composerLabel?: string
37
- inline?: boolean
38
- tools?: Array<'attach' | 'emoji'>
39
- hint?: string
40
- manual?: boolean
41
- me?: string
42
- plain?: boolean
43
- id?: string
44
- onSend?: (text: string) => void | Promise<void>
45
- }>(),
46
- {
47
- title: 'Kitchen worktop: which finish?',
48
- project: 'Villa Dewitte',
49
- meta: 'Lisa Verbeke → Tom Peeters · 14 Oct 2026',
50
- participants: () => [{ initials: 'LV', name: 'Lisa Verbeke' }, { initials: 'TP', name: 'Tom Peeters' }],
51
- status: () => ({ label: 'Urgent', variant: 'destructive' }),
52
- menu: () => [{ name: 'unread', label: 'Mark as unread' }, { name: 'archive', label: 'Move to archive' }, { separator: true }, { name: 'delete', label: 'Delete conversation', destructive: true }],
53
- messages: () => [
54
- { divider: 'Tuesday 14 October' },
55
- { sender: 'Lisa Verbeke', avatar: 'LV', time: '09:12', datetime: '2026-10-14T09:12', text: 'The supplier dropped off two worktop samples at the workshop. Photo attached.',
56
- attachments: [{ kind: 'image', href: '#samples.jpg', alt: 'Two worktop samples on the workbench', art: 6, caption: 'samples.jpg · 2.1 MB' }] },
57
- { sender: 'Lisa Verbeke', avatar: 'LV', time: '09:13', datetime: '2026-10-14T09:13', continued: true,
58
- html: '<p>Which one should we order: <strong>Calacatta matt</strong> or <strong>Nero Marquina polished</strong>? The cutting slot is Friday.</p>' },
59
- { direction: 'out', avatar: 'TP', time: '09:20', datetime: '2026-10-14T09:20', status: 'read', text: 'Calacatta matt, 20 mm. I\'ll send the updated quote to the client today.',
60
- attachments: [{ name: 'Quote kitchen Dewitte v2.pdf', size: '1.2 MB', href: '#quote.pdf' }], actions: [{ name: 'delete', label: 'Delete this message', destructive: true }] },
61
- { divider: 'Today' },
62
- { sender: 'Lisa Verbeke', avatar: 'LV', time: '10:02', datetime: '2026-10-24T10:02', text: 'Client signed. Can you confirm the edge profile before 16:00?' },
63
- ],
64
- scrollLabel: 'Messages',
65
- live: true,
66
- busyTitle: 'Sending message…',
67
- busyText: 'Please wait, do not close this conversation.',
68
- placeholder: 'Type your reply…',
69
- composerLabel: 'Reply',
70
- tools: () => ['attach', 'emoji'],
71
- hint: 'Ctrl + Enter to send',
72
- },
73
- )
74
- const emit = defineEmits<{ menu: [name: string]; tool: [name: 'attach' | 'emoji'] }>()
75
- const auto = useUid('oc-thread')
76
- const base = props.id ?? auto
77
- const list = ref<MessageListItem[]>([...props.messages])
78
- const sending = ref(false)
79
- watch(() => props.messages, (m) => (list.value = [...m]))
80
- const isOut = (m?: MessageListItem): m is MessageData => !!m && !('divider' in m) && m.direction === 'out'
81
-
82
- async function send(e: Event) {
83
- const form = e.target as HTMLFormElement
84
- const field = form.elements.namedItem('message') as HTMLTextAreaElement
85
- const text = field.value.trim()
86
- if (!text) return
87
- sending.value = true
88
- try { await props.onSend?.(text) } catch { sending.value = false; return }
89
- if (!props.manual) {
90
- const l = list.value
91
- const mine = props.me ?? [...l].reverse().find(isOut)?.avatar ?? 'Me'
92
- list.value = [...l, { direction: 'out', avatar: mine, continued: isOut(l[l.length - 1]), text, time: new Date().toTimeString().slice(0, 5), status: 'sent' }]
93
- }
94
- field.value = ''
95
- sending.value = false
96
- field.focus()
97
- }
98
- function ctrlEnter(e: KeyboardEvent) {
99
- if (e.key !== 'Enter' || !(e.ctrlKey || e.metaKey)) return
100
- e.preventDefault()
101
- ;(e.currentTarget as HTMLFormElement).requestSubmit()
102
- }
103
- </script>
104
-
105
- <template>
106
- <section :class="['oc-message-thread', plain && 'oc-message-thread--plain']" :aria-labelledby="`${base}-title`" :aria-busy="busy || sending ? 'true' : undefined">
107
- <header class="oc-message-thread__header">
108
- <slot name="back" />
109
- <div class="oc-message-thread__heading">
110
- <h3 :id="`${base}-title`" class="oc-message-thread__title">{{ title }}</h3>
111
- <p v-if="project || meta" class="oc-message-thread__meta"><strong v-if="project">{{ project }}</strong><template v-if="project && meta"> · </template>{{ meta }}</p>
112
- </div>
113
- <div v-if="participants.length" class="oc-avatar-group oc-message-thread__people" :aria-label="`${participants.length} participants`">
114
- <span v-for="(p, i) in participants" :key="i" class="oc-avatar" :title="p.name">{{ p.initials }}</span>
115
- </div>
116
- <div v-if="status || menu" class="oc-message-thread__actions">
117
- <span v-if="status" :class="['oc-badge', status.variant && `oc-badge--${status.variant}`]">{{ status.label }}</span>
118
- <template v-if="menu">
119
- <button type="button" class="oc-message-thread__icon-btn" :popovertarget="`${base}-menu`" aria-label="Conversation actions"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.8" /><circle cx="12" cy="12" r="1.8" /><circle cx="12" cy="19" r="1.8" /></svg></button>
120
- <div :id="`${base}-menu`" class="oc-dropdown-menu" popover role="menu">
121
- <template v-for="(m, i) in menu" :key="i">
122
- <div v-if="'separator' in m" class="oc-dropdown-separator" role="separator"></div>
123
- <button v-else type="button" :class="['oc-dropdown-item', m.destructive && 'oc-dropdown-item--destructive']" role="menuitem" @click="emit('menu', m.name)">{{ m.label }}</button>
124
- </template>
125
- </div>
126
- </template>
127
- </div>
128
- </header>
129
- <div class="oc-message-thread__scroll" tabindex="0" role="log" :aria-label="scrollLabel" :aria-live="live ? 'polite' : undefined">
130
- <OcMessageList :items="list" :log="false" />
131
- </div>
132
- <div class="oc-message-thread__footer">
133
- <p v-if="note" class="oc-message-thread__note">{{ note }}</p>
134
- <slot v-else name="composer">
135
- <form :class="['oc-chat-composer', inline && 'oc-chat-composer--inline']" action="#" :aria-label="composerLabel" @submit.prevent="send" @keydown="ctrlEnter">
136
- <label class="oc-visually-hidden" :for="`${base}-input`">Message</label>
137
- <textarea :id="`${base}-input`" class="oc-chat-composer__input" name="message" rows="1" :placeholder="placeholder" required></textarea>
138
- <div class="oc-chat-composer__bar">
139
- <div v-if="tools.length" class="oc-chat-composer__tools">
140
- <button v-if="tools.includes('attach')" type="button" class="oc-chat-composer__tool" aria-label="Attach a file" @click="emit('tool', 'attach')">&#128206;</button>
141
- <button v-if="tools.includes('emoji')" type="button" class="oc-chat-composer__tool" aria-label="Insert emoji" @click="emit('tool', 'emoji')">&#128578;</button>
142
- </div>
143
- <span v-if="hint" class="oc-chat-composer__hint">{{ hint }}</span>
144
- <button type="submit" class="oc-chat-composer__send" aria-label="Send">&#10148;</button>
145
- </div>
146
- </form>
147
- </slot>
148
- </div>
149
- <div v-if="busyTitle" class="oc-message-thread__busy" role="status">
150
- <span class="oc-spinner oc-spinner--lg" aria-hidden="true"></span>
151
- <strong>{{ busyTitle }}</strong>
152
- <span v-if="busyText">{{ busyText }}</span>
153
- </div>
154
- </section>
155
- </template>
1
+ <!-- File: packages/vue/src/components/OcMessageThread.vue | Map: props + demo -> state (messages, sending) -> header (back slot, heading, people, badge, menu)
2
+ -> scroll (OcMessageList) -> footer (composer slot / form / note) -> busy overlay -->
3
+ <script setup lang="ts">
4
+ /**
5
+ * Scrolling conversation (same markup as snippets/message-thread.html). Like the snippet script, a sent text is appended as an
6
+ * outgoing bubble after the @send handler (prop onSend, may return a Promise) resolves; aria-busy shows the overlay meanwhile.
7
+ * Ctrl/Cmd + Enter sends. `manual` = you append messages yourself. Emits: menu(name), tool(name).
8
+ * Slots: back (start of the header), composer (replaces the form). Classes: .oc-message-thread(--plain) __header __heading __title
9
+ * __meta __people __actions __icon-btn __scroll __footer __note __busy
10
+ */
11
+ import { ref, watch } from 'vue'
12
+ import OcMessageList, { type MessageListItem } from './OcMessageList.vue'
13
+ import type { MessageData } from './OcMessage.vue'
14
+ import { useUid } from '../composables/useUid'
15
+
16
+ export interface ThreadParticipant { initials: string; name?: string }
17
+ export type ThreadMenuItem = { name: string; label: string; destructive?: boolean } | { separator: true }
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ title?: string
22
+ project?: string
23
+ meta?: string
24
+ participants?: ThreadParticipant[]
25
+ status?: { label: string; variant?: string } | null
26
+ menu?: ThreadMenuItem[] | null
27
+ messages?: MessageListItem[]
28
+ scrollLabel?: string
29
+ live?: boolean
30
+ busy?: boolean
31
+ /** Overlay title; empty string leaves the overlay out. */
32
+ busyTitle?: string
33
+ busyText?: string
34
+ note?: string
35
+ placeholder?: string
36
+ composerLabel?: string
37
+ inline?: boolean
38
+ tools?: Array<'attach' | 'emoji'>
39
+ hint?: string
40
+ manual?: boolean
41
+ me?: string
42
+ plain?: boolean
43
+ id?: string
44
+ onSend?: (text: string) => void | Promise<void>
45
+ }>(),
46
+ {
47
+ title: 'Kitchen worktop: which finish?',
48
+ project: 'Villa Dewitte',
49
+ meta: 'Lisa Verbeke → Tom Peeters · 14 Oct 2026',
50
+ participants: () => [{ initials: 'LV', name: 'Lisa Verbeke' }, { initials: 'TP', name: 'Tom Peeters' }],
51
+ status: () => ({ label: 'Urgent', variant: 'destructive' }),
52
+ menu: () => [{ name: 'unread', label: 'Mark as unread' }, { name: 'archive', label: 'Move to archive' }, { separator: true }, { name: 'delete', label: 'Delete conversation', destructive: true }],
53
+ messages: () => [
54
+ { divider: 'Tuesday 14 October' },
55
+ { sender: 'Lisa Verbeke', avatar: 'LV', time: '09:12', datetime: '2026-10-14T09:12', text: 'The supplier dropped off two worktop samples at the workshop. Photo attached.',
56
+ attachments: [{ kind: 'image', href: '#samples.jpg', alt: 'Two worktop samples on the workbench', art: 6, caption: 'samples.jpg · 2.1 MB' }] },
57
+ { sender: 'Lisa Verbeke', avatar: 'LV', time: '09:13', datetime: '2026-10-14T09:13', continued: true,
58
+ html: '<p>Which one should we order: <strong>Calacatta matt</strong> or <strong>Nero Marquina polished</strong>? The cutting slot is Friday.</p>' },
59
+ { direction: 'out', avatar: 'TP', time: '09:20', datetime: '2026-10-14T09:20', status: 'read', text: 'Calacatta matt, 20 mm. I\'ll send the updated quote to the client today.',
60
+ attachments: [{ name: 'Quote kitchen Dewitte v2.pdf', size: '1.2 MB', href: '#quote.pdf' }], actions: [{ name: 'delete', label: 'Delete this message', destructive: true }] },
61
+ { divider: 'Today' },
62
+ { sender: 'Lisa Verbeke', avatar: 'LV', time: '10:02', datetime: '2026-10-24T10:02', text: 'Client signed. Can you confirm the edge profile before 16:00?' },
63
+ ],
64
+ scrollLabel: 'Messages',
65
+ live: true,
66
+ busyTitle: 'Sending message…',
67
+ busyText: 'Please wait, do not close this conversation.',
68
+ placeholder: 'Type your reply…',
69
+ composerLabel: 'Reply',
70
+ tools: () => ['attach', 'emoji'],
71
+ hint: 'Ctrl + Enter to send',
72
+ },
73
+ )
74
+ const emit = defineEmits<{ menu: [name: string]; tool: [name: 'attach' | 'emoji'] }>()
75
+ const auto = useUid('oc-thread')
76
+ const base = props.id ?? auto
77
+ const list = ref<MessageListItem[]>([...props.messages])
78
+ const sending = ref(false)
79
+ watch(() => props.messages, (m) => (list.value = [...m]))
80
+ const isOut = (m?: MessageListItem): m is MessageData => !!m && !('divider' in m) && m.direction === 'out'
81
+
82
+ async function send(e: Event) {
83
+ const form = e.target as HTMLFormElement
84
+ const field = form.elements.namedItem('message') as HTMLTextAreaElement
85
+ const text = field.value.trim()
86
+ if (!text) return
87
+ sending.value = true
88
+ try { await props.onSend?.(text) } catch { sending.value = false; return }
89
+ if (!props.manual) {
90
+ const l = list.value
91
+ const mine = props.me ?? [...l].reverse().find(isOut)?.avatar ?? 'Me'
92
+ list.value = [...l, { direction: 'out', avatar: mine, continued: isOut(l[l.length - 1]), text, time: new Date().toTimeString().slice(0, 5), status: 'sent' }]
93
+ }
94
+ field.value = ''
95
+ sending.value = false
96
+ field.focus()
97
+ }
98
+ function ctrlEnter(e: KeyboardEvent) {
99
+ if (e.key !== 'Enter' || !(e.ctrlKey || e.metaKey)) return
100
+ e.preventDefault()
101
+ ;(e.currentTarget as HTMLFormElement).requestSubmit()
102
+ }
103
+ </script>
104
+
105
+ <template>
106
+ <section :class="['oc-message-thread', plain && 'oc-message-thread--plain']" :aria-labelledby="`${base}-title`" :aria-busy="busy || sending ? 'true' : undefined">
107
+ <header class="oc-message-thread__header">
108
+ <slot name="back" />
109
+ <div class="oc-message-thread__heading">
110
+ <h3 :id="`${base}-title`" class="oc-message-thread__title">{{ title }}</h3>
111
+ <p v-if="project || meta" class="oc-message-thread__meta"><strong v-if="project">{{ project }}</strong><template v-if="project && meta"> · </template>{{ meta }}</p>
112
+ </div>
113
+ <div v-if="participants.length" class="oc-avatar-group oc-message-thread__people" :aria-label="`${participants.length} participants`">
114
+ <span v-for="(p, i) in participants" :key="i" class="oc-avatar" :title="p.name">{{ p.initials }}</span>
115
+ </div>
116
+ <div v-if="status || menu" class="oc-message-thread__actions">
117
+ <span v-if="status" :class="['oc-badge', status.variant && `oc-badge--${status.variant}`]">{{ status.label }}</span>
118
+ <template v-if="menu">
119
+ <button type="button" class="oc-message-thread__icon-btn" :popovertarget="`${base}-menu`" aria-label="Conversation actions"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.8" /><circle cx="12" cy="12" r="1.8" /><circle cx="12" cy="19" r="1.8" /></svg></button>
120
+ <div :id="`${base}-menu`" class="oc-dropdown-menu" popover role="menu">
121
+ <template v-for="(m, i) in menu" :key="i">
122
+ <div v-if="'separator' in m" class="oc-dropdown-separator" role="separator"></div>
123
+ <button v-else type="button" :class="['oc-dropdown-item', m.destructive && 'oc-dropdown-item--destructive']" role="menuitem" @click="emit('menu', m.name)">{{ m.label }}</button>
124
+ </template>
125
+ </div>
126
+ </template>
127
+ </div>
128
+ </header>
129
+ <div class="oc-message-thread__scroll" tabindex="0" role="log" :aria-label="scrollLabel" :aria-live="live ? 'polite' : undefined">
130
+ <OcMessageList :items="list" :log="false" />
131
+ </div>
132
+ <div class="oc-message-thread__footer">
133
+ <p v-if="note" class="oc-message-thread__note">{{ note }}</p>
134
+ <slot v-else name="composer">
135
+ <form :class="['oc-chat-composer', inline && 'oc-chat-composer--inline']" action="#" :aria-label="composerLabel" @submit.prevent="send" @keydown="ctrlEnter">
136
+ <label class="oc-visually-hidden" :for="`${base}-input`">Message</label>
137
+ <textarea :id="`${base}-input`" class="oc-chat-composer__input" name="message" rows="1" :placeholder="placeholder" required></textarea>
138
+ <div class="oc-chat-composer__bar">
139
+ <div v-if="tools.length" class="oc-chat-composer__tools">
140
+ <button v-if="tools.includes('attach')" type="button" class="oc-chat-composer__tool" aria-label="Attach a file" @click="emit('tool', 'attach')">&#128206;</button>
141
+ <button v-if="tools.includes('emoji')" type="button" class="oc-chat-composer__tool" aria-label="Insert emoji" @click="emit('tool', 'emoji')">&#128578;</button>
142
+ </div>
143
+ <span v-if="hint" class="oc-chat-composer__hint">{{ hint }}</span>
144
+ <button type="submit" class="oc-chat-composer__send" aria-label="Send">&#10148;</button>
145
+ </div>
146
+ </form>
147
+ </slot>
148
+ </div>
149
+ <div v-if="busyTitle" class="oc-message-thread__busy" role="status">
150
+ <span class="oc-spinner oc-spinner--lg" aria-hidden="true"></span>
151
+ <strong>{{ busyTitle }}</strong>
152
+ <span v-if="busyText">{{ busyText }}</span>
153
+ </div>
154
+ </section>
155
+ </template>
@@ -1,42 +1,42 @@
1
- <script setup lang="ts">
2
- /**
3
- * Modal on a native <dialog> opened with showModal(). v-model:open controls it; Esc / backdrop /
4
- * close() update the model. Slots: header (replaces title + description), default (body),
5
- * footer (scoped: { close }). Classes: .oc-modal, .oc-modal-content(--sm/--lg/--xl/--full), -header, -title,
6
- * -description, -body, -footer.
7
- */
8
- import { ref } from 'vue'
9
- import { useDialog } from '../composables/useDialog'
10
- import { useUid } from '../composables/useUid'
11
-
12
- withDefaults(
13
- defineProps<{
14
- title?: string
15
- description?: string
16
- size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
17
- closedby?: 'any' | 'closerequest' | 'none'
18
- }>(),
19
- { size: 'md', closedby: 'any' },
20
- )
21
- const open = defineModel<boolean>('open', { default: false })
22
- const el = ref<HTMLDialogElement | null>(null)
23
- const { onClose } = useDialog(open, el)
24
- const titleId = useUid('oc-modal-title')
25
- const close = () => (open.value = false)
26
- defineExpose({ close })
27
- </script>
28
-
29
- <template>
30
- <dialog ref="el" class="oc-modal" :closedby="closedby" :aria-labelledby="title ? titleId : undefined" @close="onClose">
31
- <div :class="['oc-modal-content', size !== 'md' && `oc-modal-content--${size}`]">
32
- <div v-if="title || description || $slots.header" class="oc-modal-header">
33
- <slot name="header">
34
- <h2 v-if="title" :id="titleId" class="oc-modal-title">{{ title }}</h2>
35
- <p v-if="description" class="oc-modal-description">{{ description }}</p>
36
- </slot>
37
- </div>
38
- <div v-if="$slots.default" class="oc-modal-body"><slot /></div>
39
- <div v-if="$slots.footer" class="oc-modal-footer"><slot name="footer" :close="close" /></div>
40
- </div>
41
- </dialog>
42
- </template>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Modal on a native <dialog> opened with showModal(). v-model:open controls it; Esc / backdrop /
4
+ * close() update the model. Slots: header (replaces title + description), default (body),
5
+ * footer (scoped: { close }). Classes: .oc-modal, .oc-modal-content(--sm/--lg/--xl/--full), -header, -title,
6
+ * -description, -body, -footer.
7
+ */
8
+ import { ref } from 'vue'
9
+ import { useDialog } from '../composables/useDialog'
10
+ import { useUid } from '../composables/useUid'
11
+
12
+ withDefaults(
13
+ defineProps<{
14
+ title?: string
15
+ description?: string
16
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
17
+ closedby?: 'any' | 'closerequest' | 'none'
18
+ }>(),
19
+ { size: 'md', closedby: 'any' },
20
+ )
21
+ const open = defineModel<boolean>('open', { default: false })
22
+ const el = ref<HTMLDialogElement | null>(null)
23
+ const { onClose } = useDialog(open, el)
24
+ const titleId = useUid('oc-modal-title')
25
+ const close = () => (open.value = false)
26
+ defineExpose({ close })
27
+ </script>
28
+
29
+ <template>
30
+ <dialog ref="el" class="oc-modal" :closedby="closedby" :aria-labelledby="title ? titleId : undefined" @close="onClose">
31
+ <div :class="['oc-modal-content', size !== 'md' && `oc-modal-content--${size}`]">
32
+ <div v-if="title || description || $slots.header" class="oc-modal-header">
33
+ <slot name="header">
34
+ <h2 v-if="title" :id="titleId" class="oc-modal-title">{{ title }}</h2>
35
+ <p v-if="description" class="oc-modal-description">{{ description }}</p>
36
+ </slot>
37
+ </div>
38
+ <div v-if="$slots.default" class="oc-modal-body"><slot /></div>
39
+ <div v-if="$slots.footer" class="oc-modal-footer"><slot name="footer" :close="close" /></div>
40
+ </div>
41
+ </dialog>
42
+ </template>
@@ -1,18 +1,18 @@
1
- <script setup lang="ts">
2
- /**
3
- * Horizontal navigation links with optional dropdown children, wrapped in a <nav>.
4
- * Pass `items` (see OcNavmenuList) or put your own <li> elements in the default slot.
5
- */
6
- import OcNavmenuList, { type NavmenuItem } from './OcNavmenuList.vue'
7
-
8
- withDefaults(defineProps<{ items?: NavmenuItem[]; label?: string }>(), {
9
- items: () => [],
10
- label: 'Navigation',
11
- })
12
- </script>
13
-
14
- <template>
15
- <nav :aria-label="label">
16
- <OcNavmenuList :items="items"><slot /></OcNavmenuList>
17
- </nav>
18
- </template>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Horizontal navigation links with optional dropdown children, wrapped in a <nav>.
4
+ * Pass `items` (see OcNavmenuList) or put your own <li> elements in the default slot.
5
+ */
6
+ import OcNavmenuList, { type NavmenuItem } from './OcNavmenuList.vue'
7
+
8
+ withDefaults(defineProps<{ items?: NavmenuItem[]; label?: string }>(), {
9
+ items: () => [],
10
+ label: 'Navigation',
11
+ })
12
+ </script>
13
+
14
+ <template>
15
+ <nav :aria-label="label">
16
+ <OcNavmenuList :items="items"><slot /></OcNavmenuList>
17
+ </nav>
18
+ </template>
@@ -1,38 +1,38 @@
1
- <script setup lang="ts">
2
- /**
3
- * The <ul class="oc-navmenu"> of the navigation menu, without the surrounding <nav>
4
- * (used by OcNavmenu and by blocks that provide their own <nav>).
5
- * Pass `items`, or put your own <li> elements in the default slot.
6
- * Classes: .oc-navmenu, .oc-navmenu-link(--caret) with aria-current="page"
7
- */
8
- import { useUid } from '../composables/useUid'
9
-
10
- export interface NavmenuItem {
11
- label: string
12
- href?: string
13
- current?: boolean
14
- /** Dropdown entries; turns the item into a popover trigger. */
15
- children?: { label: string; href: string }[]
16
- /** Heading inside the dropdown. */
17
- heading?: string
18
- }
19
- withDefaults(defineProps<{ items?: NavmenuItem[] }>(), { items: () => [] })
20
- const base = useUid('oc-navmenu')
21
- </script>
22
-
23
- <template>
24
- <ul class="oc-navmenu">
25
- <slot>
26
- <li v-for="(item, i) in items" :key="i">
27
- <template v-if="item.children?.length">
28
- <button class="oc-navmenu-link oc-navmenu-link--caret" type="button" :popovertarget="`${base}-${i}`">{{ item.label }}</button>
29
- <div :id="`${base}-${i}`" class="oc-dropdown-menu" popover>
30
- <div v-if="item.heading" class="oc-dropdown-label">{{ item.heading }}</div>
31
- <a v-for="child in item.children" :key="child.href + child.label" class="oc-dropdown-item" :href="child.href">{{ child.label }}</a>
32
- </div>
33
- </template>
34
- <a v-else class="oc-navmenu-link" :href="item.href ?? '#'" :aria-current="item.current ? 'page' : undefined">{{ item.label }}</a>
35
- </li>
36
- </slot>
37
- </ul>
38
- </template>
1
+ <script setup lang="ts">
2
+ /**
3
+ * The <ul class="oc-navmenu"> of the navigation menu, without the surrounding <nav>
4
+ * (used by OcNavmenu and by blocks that provide their own <nav>).
5
+ * Pass `items`, or put your own <li> elements in the default slot.
6
+ * Classes: .oc-navmenu, .oc-navmenu-link(--caret) with aria-current="page"
7
+ */
8
+ import { useUid } from '../composables/useUid'
9
+
10
+ export interface NavmenuItem {
11
+ label: string
12
+ href?: string
13
+ current?: boolean
14
+ /** Dropdown entries; turns the item into a popover trigger. */
15
+ children?: { label: string; href: string }[]
16
+ /** Heading inside the dropdown. */
17
+ heading?: string
18
+ }
19
+ withDefaults(defineProps<{ items?: NavmenuItem[] }>(), { items: () => [] })
20
+ const base = useUid('oc-navmenu')
21
+ </script>
22
+
23
+ <template>
24
+ <ul class="oc-navmenu">
25
+ <slot>
26
+ <li v-for="(item, i) in items" :key="i">
27
+ <template v-if="item.children?.length">
28
+ <button class="oc-navmenu-link oc-navmenu-link--caret" type="button" :popovertarget="`${base}-${i}`">{{ item.label }}</button>
29
+ <div :id="`${base}-${i}`" class="oc-dropdown-menu" popover>
30
+ <div v-if="item.heading" class="oc-dropdown-label">{{ item.heading }}</div>
31
+ <a v-for="child in item.children" :key="child.href + child.label" class="oc-dropdown-item" :href="child.href">{{ child.label }}</a>
32
+ </div>
33
+ </template>
34
+ <a v-else class="oc-navmenu-link" :href="item.href ?? '#'" :aria-current="item.current ? 'page' : undefined">{{ item.label }}</a>
35
+ </li>
36
+ </slot>
37
+ </ul>
38
+ </template>