@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.
- package/LICENSE +21 -21
- package/README.md +49 -49
- package/dist/omniaui-vue.js.map +1 -1
- package/package.json +69 -69
- package/src/blocks/OcBlockAboutTimeline.vue +44 -44
- package/src/blocks/OcBlockAppHeader.vue +36 -36
- package/src/blocks/OcBlockAppShell.vue +29 -29
- package/src/blocks/OcBlockAuth.vue +24 -24
- package/src/blocks/OcBlockAuthSplit.vue +47 -47
- package/src/blocks/OcBlockContactGrid.vue +53 -53
- package/src/blocks/OcBlockCta.vue +12 -12
- package/src/blocks/OcBlockDataTable.vue +27 -27
- package/src/blocks/OcBlockFaq.vue +49 -49
- package/src/blocks/OcBlockFeatures.vue +23 -23
- package/src/blocks/OcBlockFooter.vue +25 -25
- package/src/blocks/OcBlockFormPage.vue +24 -24
- package/src/blocks/OcBlockHero.vue +13 -13
- package/src/blocks/OcBlockHeroCode.vue +42 -42
- package/src/blocks/OcBlockHeroOrbit.vue +69 -69
- package/src/blocks/OcBlockHeroSplit.vue +15 -15
- package/src/blocks/OcBlockModuleHeader.vue +21 -21
- package/src/blocks/OcBlockPageHero.vue +13 -13
- package/src/blocks/OcBlockPortfolioGrid.vue +63 -63
- package/src/blocks/OcBlockPricing.vue +39 -39
- package/src/blocks/OcBlockProcess.vue +39 -39
- package/src/blocks/OcBlockProductGrid.vue +41 -41
- package/src/blocks/OcBlockProfileHeader.vue +50 -50
- package/src/blocks/OcBlockShowcase.vue +43 -43
- package/src/blocks/OcBlockSiteFooter.vue +54 -54
- package/src/blocks/OcBlockSiteHeader.vue +34 -34
- package/src/blocks/OcBlockSpotlight.vue +47 -47
- package/src/blocks/OcBlockStats.vue +17 -17
- package/src/blocks/OcBlockStatsCounter.vue +20 -20
- package/src/blocks/OcBlockTeamList.vue +53 -53
- package/src/blocks/OcBlockTwoFactor.vue +61 -61
- package/src/blocks/OcBlockWizardDialog.vue +67 -67
- package/src/components/OcAccordion.vue +12 -12
- package/src/components/OcAccordionAccent.vue +27 -27
- package/src/components/OcAccordionItem.vue +19 -19
- package/src/components/OcAffixInput.vue +40 -40
- package/src/components/OcAlert.vue +17 -17
- package/src/components/OcAlertDialog.vue +66 -66
- package/src/components/OcAlphaTabs.vue +54 -54
- package/src/components/OcAspectRatio.vue +21 -21
- package/src/components/OcAttachment.vue +59 -59
- package/src/components/OcAttachmentImage.vue +13 -13
- package/src/components/OcAttachmentList.vue +31 -31
- package/src/components/OcAttachmentThumb.vue +14 -14
- package/src/components/OcAvatar.vue +18 -18
- package/src/components/OcAvatarGroup.vue +7 -7
- package/src/components/OcBackLink.vue +8 -8
- package/src/components/OcBadge.vue +13 -13
- package/src/components/OcBarcodeInput.vue +51 -51
- package/src/components/OcBreadcrumb.vue +22 -22
- package/src/components/OcBrowserFrame.vue +33 -33
- package/src/components/OcBusyOverlay.vue +32 -32
- package/src/components/OcButton.vue +47 -47
- package/src/components/OcButtonGroup.vue +10 -10
- package/src/components/OcButtonGroupText.vue +7 -7
- package/src/components/OcCard.vue +20 -20
- package/src/components/OcChartLine.vue +80 -80
- package/src/components/OcChatComposer.vue +42 -42
- package/src/components/OcCheckbox.vue +26 -26
- package/src/components/OcChecklist.vue +29 -29
- package/src/components/OcCollapsible.vue +19 -19
- package/src/components/OcColorField.vue +15 -15
- package/src/components/OcColorSelect.vue +33 -33
- package/src/components/OcColumnMenu.vue +50 -50
- package/src/components/OcCommand.vue +119 -119
- package/src/components/OcConfirmDialog.vue +65 -65
- package/src/components/OcCookieConsent.vue +119 -119
- package/src/components/OcCopyField.vue +37 -37
- package/src/components/OcCounter.vue +15 -15
- package/src/components/OcDataTable.vue +144 -144
- package/src/components/OcDefinitionList.vue +35 -35
- package/src/components/OcDropdown.vue +23 -23
- package/src/components/OcDropdownItem.vue +9 -9
- package/src/components/OcDropdownLabel.vue +7 -7
- package/src/components/OcDropdownSeparator.vue +7 -7
- package/src/components/OcDropzone.vue +36 -36
- package/src/components/OcEmojiPicker.vue +45 -45
- package/src/components/OcEmptyState.vue +13 -13
- package/src/components/OcEntityCard.vue +47 -47
- package/src/components/OcFactList.vue +37 -37
- package/src/components/OcFilterCounts.vue +38 -38
- package/src/components/OcFlipCard.vue +36 -36
- package/src/components/OcFormField.vue +36 -36
- package/src/components/OcGreeting.vue +52 -52
- package/src/components/OcHeading.vue +19 -19
- package/src/components/OcHoverCard.vue +25 -25
- package/src/components/OcImageZoom.vue +39 -39
- package/src/components/OcInput.vue +9 -9
- package/src/components/OcInputGroup.vue +12 -12
- package/src/components/OcItem.vue +40 -40
- package/src/components/OcItemGroup.vue +7 -7
- package/src/components/OcItemSeparator.vue +7 -7
- package/src/components/OcKbd.vue +7 -7
- package/src/components/OcKbdGroup.vue +15 -15
- package/src/components/OcLabel.vue +10 -10
- package/src/components/OcLanguageSelect.vue +40 -40
- package/src/components/OcLauncherCard.vue +34 -34
- package/src/components/OcLightbox.vue +63 -63
- package/src/components/OcLinkArrow.vue +25 -25
- package/src/components/OcListToolbar.vue +44 -44
- package/src/components/OcMarquee.vue +36 -36
- package/src/components/OcMediaCard.vue +28 -28
- package/src/components/OcMediaRow.vue +28 -28
- package/src/components/OcMediaThumb.vue +38 -38
- package/src/components/OcMessage.vue +91 -91
- package/src/components/OcMessageList.vue +43 -43
- package/src/components/OcMessageThread.vue +155 -155
- package/src/components/OcModal.vue +42 -42
- package/src/components/OcNavmenu.vue +18 -18
- package/src/components/OcNavmenuList.vue +38 -38
- package/src/components/OcNextTeaser.vue +25 -25
- package/src/components/OcNumberInput.vue +47 -47
- package/src/components/OcOtpInput.vue +34 -34
- package/src/components/OcPageHeader.vue +34 -34
- package/src/components/OcPagination.vue +33 -33
- package/src/components/OcPasswordInput.vue +27 -27
- package/src/components/OcPasswordRequirements.vue +28 -28
- package/src/components/OcPhoneInput.vue +72 -72
- package/src/components/OcPopover.vue +25 -25
- package/src/components/OcPriceCard.vue +69 -69
- package/src/components/OcProductMark.vue +31 -31
- package/src/components/OcProgress.vue +21 -21
- package/src/components/OcProjectCard.vue +36 -36
- package/src/components/OcProse.vue +57 -57
- package/src/components/OcRadio.vue +40 -40
- package/src/components/OcRadioGroup.vue +32 -32
- package/src/components/OcRecoveryCodes.vue +55 -55
- package/src/components/OcScrollArea.vue +15 -15
- package/src/components/OcSegmented.vue +28 -28
- package/src/components/OcSelect.vue +21 -21
- package/src/components/OcSeparator.vue +18 -18
- package/src/components/OcSeverityCard.vue +46 -46
- package/src/components/OcSheet.vue +49 -49
- package/src/components/OcSidebarCollapsible.vue +19 -19
- package/src/components/OcSidebarGroup.vue +11 -11
- package/src/components/OcSidebarMenu.vue +7 -7
- package/src/components/OcSidebarMenuButton.vue +31 -31
- package/src/components/OcSidebarSeparator.vue +7 -7
- package/src/components/OcSkeleton.vue +8 -8
- package/src/components/OcSlider.vue +27 -27
- package/src/components/OcSortableRow.vue +40 -40
- package/src/components/OcSpinner.vue +13 -13
- package/src/components/OcStackCard.vue +24 -24
- package/src/components/OcStackList.vue +31 -31
- package/src/components/OcStatCard.vue +39 -39
- package/src/components/OcStepper.vue +34 -34
- package/src/components/OcSwitch.vue +15 -15
- package/src/components/OcTable.vue +115 -115
- package/src/components/OcTableActions.vue +26 -26
- package/src/components/OcTableCompact.vue +46 -46
- package/src/components/OcTabs.vue +37 -37
- package/src/components/OcTagInput.vue +50 -50
- package/src/components/OcTerminalLog.vue +50 -50
- package/src/components/OcTextarea.vue +9 -9
- package/src/components/OcThemeSwitch.vue +31 -31
- package/src/components/OcTimeInput.vue +50 -50
- package/src/components/OcTimeline.vue +36 -36
- package/src/components/OcToast.vue +31 -31
- package/src/components/OcToastContainer.vue +21 -21
- package/src/components/OcToggle.vue +63 -63
- package/src/components/OcToggleField.vue +28 -28
- package/src/components/OcToggleGroup.vue +40 -40
- package/src/components/OcTooltip.vue +8 -8
- package/src/components/OcTopProgress.vue +54 -54
- package/src/components/OcTree.vue +70 -70
- package/src/components/OcUpgradeBanner.vue +42 -42
- package/src/components/OcUser.vue +25 -25
- package/src/components/OcVersionBanner.vue +34 -34
- package/src/composables/chartLine.ts +28 -28
- package/src/composables/keys.ts +22 -22
- package/src/composables/useAttrsSplit.ts +16 -16
- package/src/composables/useDialog.ts +21 -21
- package/src/composables/useToast.ts +66 -66
- package/src/composables/useUid.ts +12 -12
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcLinkArrow.vue | Map: props (direction, tone, size) -> a.oc-link-arrow -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
/** Text link with a moving arrow. Classes: .oc-link-arrow + --back/--external, --accent/--muted, --sm/--lg */
|
|
4
|
-
withDefaults(
|
|
5
|
-
defineProps<{
|
|
6
|
-
href: string
|
|
7
|
-
direction?: 'forward' | 'back' | 'external'
|
|
8
|
-
tone?: 'default' | 'accent' | 'muted'
|
|
9
|
-
size?: 'sm' | 'default' | 'lg'
|
|
10
|
-
}>(),
|
|
11
|
-
{ direction: 'forward', tone: 'default', size: 'default' },
|
|
12
|
-
)
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<template>
|
|
16
|
-
<a
|
|
17
|
-
:class="[
|
|
18
|
-
'oc-link-arrow',
|
|
19
|
-
direction !== 'forward' && `oc-link-arrow--${direction}`,
|
|
20
|
-
tone !== 'default' && `oc-link-arrow--${tone}`,
|
|
21
|
-
size !== 'default' && `oc-link-arrow--${size}`,
|
|
22
|
-
]"
|
|
23
|
-
:href="href"
|
|
24
|
-
><slot /></a>
|
|
25
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcLinkArrow.vue | Map: props (direction, tone, size) -> a.oc-link-arrow -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
/** Text link with a moving arrow. Classes: .oc-link-arrow + --back/--external, --accent/--muted, --sm/--lg */
|
|
4
|
+
withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
href: string
|
|
7
|
+
direction?: 'forward' | 'back' | 'external'
|
|
8
|
+
tone?: 'default' | 'accent' | 'muted'
|
|
9
|
+
size?: 'sm' | 'default' | 'lg'
|
|
10
|
+
}>(),
|
|
11
|
+
{ direction: 'forward', tone: 'default', size: 'default' },
|
|
12
|
+
)
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<a
|
|
17
|
+
:class="[
|
|
18
|
+
'oc-link-arrow',
|
|
19
|
+
direction !== 'forward' && `oc-link-arrow--${direction}`,
|
|
20
|
+
tone !== 'default' && `oc-link-arrow--${tone}`,
|
|
21
|
+
size !== 'default' && `oc-link-arrow--${size}`,
|
|
22
|
+
]"
|
|
23
|
+
:href="href"
|
|
24
|
+
><slot /></a>
|
|
25
|
+
</template>
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcListToolbar.vue | Map: toolbar row -> search slot -> count -> spacer -> view radios (built-in icons) -> actions slot -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* Toolbar above a list. v-model:view picks the view ("cards", "table" or "list"); render the matching view yourself.
|
|
5
|
-
* Slots: search, actions, view-icon (scoped { view } to replace the built-in icons).
|
|
6
|
-
* Classes: .oc-list-toolbar(--sticky/--bordered), -search, -count, -spacer, -views, -view, -actions
|
|
7
|
-
*/
|
|
8
|
-
import { useUid } from '../composables/useUid'
|
|
9
|
-
|
|
10
|
-
withDefaults(
|
|
11
|
-
defineProps<{
|
|
12
|
-
count?: string
|
|
13
|
-
views?: Array<{ value: string; label: string }>
|
|
14
|
-
viewsLabel?: string
|
|
15
|
-
sticky?: boolean
|
|
16
|
-
bordered?: boolean
|
|
17
|
-
}>(),
|
|
18
|
-
{ views: () => [], viewsLabel: 'View' },
|
|
19
|
-
)
|
|
20
|
-
const view = defineModel<string>('view', { default: 'cards' })
|
|
21
|
-
const name = useUid('oc-list-view')
|
|
22
|
-
</script>
|
|
23
|
-
|
|
24
|
-
<template>
|
|
25
|
-
<div :class="['oc-list-toolbar', sticky && 'oc-list-toolbar--sticky', bordered && 'oc-list-toolbar--bordered']" role="toolbar">
|
|
26
|
-
<div v-if="$slots.search" class="oc-list-toolbar-search"><slot name="search" /></div>
|
|
27
|
-
<span v-if="count" class="oc-list-toolbar-count">{{ count }}</span>
|
|
28
|
-
<span class="oc-list-toolbar-spacer"></span>
|
|
29
|
-
<div v-if="views.length" class="oc-list-toolbar-views" role="radiogroup" :aria-label="viewsLabel">
|
|
30
|
-
<label v-for="v in views" :key="v.value" class="oc-list-toolbar-view" :title="v.label">
|
|
31
|
-
<input v-model="view" type="radio" :name="name" :value="v.value" :aria-label="v.label" />
|
|
32
|
-
<slot name="view-icon" :view="v">
|
|
33
|
-
<!-- Built-in icons for the three common views. -->
|
|
34
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
35
|
-
<template v-if="v.value === 'cards'"><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></template>
|
|
36
|
-
<path v-else-if="v.value === 'table'" d="M3 5h18M3 12h18M3 19h18" />
|
|
37
|
-
<path v-else d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
|
38
|
-
</svg>
|
|
39
|
-
</slot>
|
|
40
|
-
</label>
|
|
41
|
-
</div>
|
|
42
|
-
<div v-if="$slots.actions" class="oc-list-toolbar-actions"><slot name="actions" /></div>
|
|
43
|
-
</div>
|
|
44
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcListToolbar.vue | Map: toolbar row -> search slot -> count -> spacer -> view radios (built-in icons) -> actions slot -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* Toolbar above a list. v-model:view picks the view ("cards", "table" or "list"); render the matching view yourself.
|
|
5
|
+
* Slots: search, actions, view-icon (scoped { view } to replace the built-in icons).
|
|
6
|
+
* Classes: .oc-list-toolbar(--sticky/--bordered), -search, -count, -spacer, -views, -view, -actions
|
|
7
|
+
*/
|
|
8
|
+
import { useUid } from '../composables/useUid'
|
|
9
|
+
|
|
10
|
+
withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
count?: string
|
|
13
|
+
views?: Array<{ value: string; label: string }>
|
|
14
|
+
viewsLabel?: string
|
|
15
|
+
sticky?: boolean
|
|
16
|
+
bordered?: boolean
|
|
17
|
+
}>(),
|
|
18
|
+
{ views: () => [], viewsLabel: 'View' },
|
|
19
|
+
)
|
|
20
|
+
const view = defineModel<string>('view', { default: 'cards' })
|
|
21
|
+
const name = useUid('oc-list-view')
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div :class="['oc-list-toolbar', sticky && 'oc-list-toolbar--sticky', bordered && 'oc-list-toolbar--bordered']" role="toolbar">
|
|
26
|
+
<div v-if="$slots.search" class="oc-list-toolbar-search"><slot name="search" /></div>
|
|
27
|
+
<span v-if="count" class="oc-list-toolbar-count">{{ count }}</span>
|
|
28
|
+
<span class="oc-list-toolbar-spacer"></span>
|
|
29
|
+
<div v-if="views.length" class="oc-list-toolbar-views" role="radiogroup" :aria-label="viewsLabel">
|
|
30
|
+
<label v-for="v in views" :key="v.value" class="oc-list-toolbar-view" :title="v.label">
|
|
31
|
+
<input v-model="view" type="radio" :name="name" :value="v.value" :aria-label="v.label" />
|
|
32
|
+
<slot name="view-icon" :view="v">
|
|
33
|
+
<!-- Built-in icons for the three common views. -->
|
|
34
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
35
|
+
<template v-if="v.value === 'cards'"><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></template>
|
|
36
|
+
<path v-else-if="v.value === 'table'" d="M3 5h18M3 12h18M3 19h18" />
|
|
37
|
+
<path v-else d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
|
38
|
+
</svg>
|
|
39
|
+
</slot>
|
|
40
|
+
</label>
|
|
41
|
+
</div>
|
|
42
|
+
<div v-if="$slots.actions" class="oc-list-toolbar-actions"><slot name="actions" /></div>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcMarquee.vue | Map: props -> .oc-marquee -> track (items + aria-hidden copy) -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* Endless strip; pauses on hover / focus, stands still with reduced motion. The items are rendered twice
|
|
5
|
-
* (the copy is aria-hidden). Classes: .oc-marquee (--reverse, --slow, --fast, --plain, --no-fade), __track, __item
|
|
6
|
-
*/
|
|
7
|
-
withDefaults(
|
|
8
|
-
defineProps<{
|
|
9
|
-
items: string[]
|
|
10
|
-
label?: string
|
|
11
|
-
reverse?: boolean
|
|
12
|
-
speed?: 'default' | 'slow' | 'fast'
|
|
13
|
-
plain?: boolean
|
|
14
|
-
noFade?: boolean
|
|
15
|
-
}>(),
|
|
16
|
-
{ speed: 'default' },
|
|
17
|
-
)
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<template>
|
|
21
|
-
<div
|
|
22
|
-
:class="[
|
|
23
|
-
'oc-marquee',
|
|
24
|
-
reverse && 'oc-marquee--reverse',
|
|
25
|
-
speed !== 'default' && `oc-marquee--${speed}`,
|
|
26
|
-
plain && 'oc-marquee--plain',
|
|
27
|
-
noFade && 'oc-marquee--no-fade',
|
|
28
|
-
]"
|
|
29
|
-
:aria-label="label"
|
|
30
|
-
>
|
|
31
|
-
<div class="oc-marquee__track">
|
|
32
|
-
<span v-for="(item, i) in items" :key="i" class="oc-marquee__item">{{ item }}</span>
|
|
33
|
-
<span v-for="(item, i) in items" :key="'copy-' + i" class="oc-marquee__item" aria-hidden="true">{{ item }}</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcMarquee.vue | Map: props -> .oc-marquee -> track (items + aria-hidden copy) -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* Endless strip; pauses on hover / focus, stands still with reduced motion. The items are rendered twice
|
|
5
|
+
* (the copy is aria-hidden). Classes: .oc-marquee (--reverse, --slow, --fast, --plain, --no-fade), __track, __item
|
|
6
|
+
*/
|
|
7
|
+
withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
items: string[]
|
|
10
|
+
label?: string
|
|
11
|
+
reverse?: boolean
|
|
12
|
+
speed?: 'default' | 'slow' | 'fast'
|
|
13
|
+
plain?: boolean
|
|
14
|
+
noFade?: boolean
|
|
15
|
+
}>(),
|
|
16
|
+
{ speed: 'default' },
|
|
17
|
+
)
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div
|
|
22
|
+
:class="[
|
|
23
|
+
'oc-marquee',
|
|
24
|
+
reverse && 'oc-marquee--reverse',
|
|
25
|
+
speed !== 'default' && `oc-marquee--${speed}`,
|
|
26
|
+
plain && 'oc-marquee--plain',
|
|
27
|
+
noFade && 'oc-marquee--no-fade',
|
|
28
|
+
]"
|
|
29
|
+
:aria-label="label"
|
|
30
|
+
>
|
|
31
|
+
<div class="oc-marquee__track">
|
|
32
|
+
<span v-for="(item, i) in items" :key="i" class="oc-marquee__item">{{ item }}</span>
|
|
33
|
+
<span v-for="(item, i) in items" :key="'copy-' + i" class="oc-marquee__item" aria-hidden="true">{{ item }}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcMediaCard.vue | Map: props -> article -> OcMediaThumb -> body (avatar, title + meta, menu) -> popover menu -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* Video feed card (same markup as snippets/media-card.html). Props: title, href (title link covers the card), meta,
|
|
5
|
-
* avatar (initials), thumb (props for OcMediaThumb), compact. Slots: avatar (replaces initials), meta, menu (dropdown items;
|
|
6
|
-
* shows the ⋮ button). Classes: .oc-media-card(--compact), __body, __avatar, __text, __title, __meta, __menu
|
|
7
|
-
*/
|
|
8
|
-
import OcMediaThumb from './OcMediaThumb.vue'
|
|
9
|
-
import { useUid } from '../composables/useUid'
|
|
10
|
-
|
|
11
|
-
defineProps<{ title: string; href: string; meta?: string; avatar?: string; thumb?: Record<string, unknown>; compact?: boolean }>()
|
|
12
|
-
const menuId = useUid('oc-media-card-menu')
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<template>
|
|
16
|
-
<article :class="['oc-media-card', compact && 'oc-media-card--compact']">
|
|
17
|
-
<OcMediaThumb v-bind="thumb" />
|
|
18
|
-
<div class="oc-media-card__body">
|
|
19
|
-
<span v-if="avatar || $slots.avatar" class="oc-media-card__avatar" aria-hidden="true"><slot name="avatar">{{ avatar }}</slot></span>
|
|
20
|
-
<div class="oc-media-card__text">
|
|
21
|
-
<h3 class="oc-media-card__title"><a :href="href">{{ title }}</a></h3>
|
|
22
|
-
<p v-if="meta || $slots.meta" class="oc-media-card__meta"><slot name="meta">{{ meta }}</slot></p>
|
|
23
|
-
</div>
|
|
24
|
-
<button v-if="$slots.menu" type="button" class="oc-media-card__menu" :popovertarget="menuId" aria-label="More actions">⋮</button>
|
|
25
|
-
</div>
|
|
26
|
-
<div v-if="$slots.menu" :id="menuId" class="oc-dropdown-menu" popover><slot name="menu" /></div>
|
|
27
|
-
</article>
|
|
28
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcMediaCard.vue | Map: props -> article -> OcMediaThumb -> body (avatar, title + meta, menu) -> popover menu -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* Video feed card (same markup as snippets/media-card.html). Props: title, href (title link covers the card), meta,
|
|
5
|
+
* avatar (initials), thumb (props for OcMediaThumb), compact. Slots: avatar (replaces initials), meta, menu (dropdown items;
|
|
6
|
+
* shows the ⋮ button). Classes: .oc-media-card(--compact), __body, __avatar, __text, __title, __meta, __menu
|
|
7
|
+
*/
|
|
8
|
+
import OcMediaThumb from './OcMediaThumb.vue'
|
|
9
|
+
import { useUid } from '../composables/useUid'
|
|
10
|
+
|
|
11
|
+
defineProps<{ title: string; href: string; meta?: string; avatar?: string; thumb?: Record<string, unknown>; compact?: boolean }>()
|
|
12
|
+
const menuId = useUid('oc-media-card-menu')
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<article :class="['oc-media-card', compact && 'oc-media-card--compact']">
|
|
17
|
+
<OcMediaThumb v-bind="thumb" />
|
|
18
|
+
<div class="oc-media-card__body">
|
|
19
|
+
<span v-if="avatar || $slots.avatar" class="oc-media-card__avatar" aria-hidden="true"><slot name="avatar">{{ avatar }}</slot></span>
|
|
20
|
+
<div class="oc-media-card__text">
|
|
21
|
+
<h3 class="oc-media-card__title"><a :href="href">{{ title }}</a></h3>
|
|
22
|
+
<p v-if="meta || $slots.meta" class="oc-media-card__meta"><slot name="meta">{{ meta }}</slot></p>
|
|
23
|
+
</div>
|
|
24
|
+
<button v-if="$slots.menu" type="button" class="oc-media-card__menu" :popovertarget="menuId" aria-label="More actions">⋮</button>
|
|
25
|
+
</div>
|
|
26
|
+
<div v-if="$slots.menu" :id="menuId" class="oc-dropdown-menu" popover><slot name="menu" /></div>
|
|
27
|
+
</article>
|
|
28
|
+
</template>
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcMediaRow.vue | Map: props -> li row -> OcMediaThumb -> title link + meta -> menu button + popover -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* Compact media row (same markup as snippets/media-row.html). Put rows in
|
|
5
|
-
* <ul class="oc-media-row-list"> or <ol class="oc-media-row-list oc-media-row-list--numbered"> (numbered playlist).
|
|
6
|
-
* Props: title, href (link covers the row), meta, thumb (props for OcMediaThumb), current (aria-current).
|
|
7
|
-
* Slots: meta, menu (dropdown items; shows the ⋮ button). Classes: .oc-media-row, __text, __title, __meta, __menu
|
|
8
|
-
*/
|
|
9
|
-
import OcMediaThumb from './OcMediaThumb.vue'
|
|
10
|
-
import { useUid } from '../composables/useUid'
|
|
11
|
-
|
|
12
|
-
defineProps<{ title: string; href: string; meta?: string; thumb?: Record<string, unknown>; current?: boolean }>()
|
|
13
|
-
const menuId = useUid('oc-media-row-menu')
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<li class="oc-media-row" :aria-current="current ? 'true' : undefined">
|
|
18
|
-
<OcMediaThumb v-bind="thumb" />
|
|
19
|
-
<div class="oc-media-row__text">
|
|
20
|
-
<h3 class="oc-media-row__title"><a :href="href">{{ title }}</a></h3>
|
|
21
|
-
<p v-if="meta || $slots.meta" class="oc-media-row__meta"><slot name="meta">{{ meta }}</slot></p>
|
|
22
|
-
</div>
|
|
23
|
-
<template v-if="$slots.menu">
|
|
24
|
-
<button type="button" class="oc-media-row__menu" :popovertarget="menuId" aria-label="More actions">⋮</button>
|
|
25
|
-
<div :id="menuId" class="oc-dropdown-menu" popover><slot name="menu" /></div>
|
|
26
|
-
</template>
|
|
27
|
-
</li>
|
|
28
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcMediaRow.vue | Map: props -> li row -> OcMediaThumb -> title link + meta -> menu button + popover -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* Compact media row (same markup as snippets/media-row.html). Put rows in
|
|
5
|
+
* <ul class="oc-media-row-list"> or <ol class="oc-media-row-list oc-media-row-list--numbered"> (numbered playlist).
|
|
6
|
+
* Props: title, href (link covers the row), meta, thumb (props for OcMediaThumb), current (aria-current).
|
|
7
|
+
* Slots: meta, menu (dropdown items; shows the ⋮ button). Classes: .oc-media-row, __text, __title, __meta, __menu
|
|
8
|
+
*/
|
|
9
|
+
import OcMediaThumb from './OcMediaThumb.vue'
|
|
10
|
+
import { useUid } from '../composables/useUid'
|
|
11
|
+
|
|
12
|
+
defineProps<{ title: string; href: string; meta?: string; thumb?: Record<string, unknown>; current?: boolean }>()
|
|
13
|
+
const menuId = useUid('oc-media-row-menu')
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<li class="oc-media-row" :aria-current="current ? 'true' : undefined">
|
|
18
|
+
<OcMediaThumb v-bind="thumb" />
|
|
19
|
+
<div class="oc-media-row__text">
|
|
20
|
+
<h3 class="oc-media-row__title"><a :href="href">{{ title }}</a></h3>
|
|
21
|
+
<p v-if="meta || $slots.meta" class="oc-media-row__meta"><slot name="meta">{{ meta }}</slot></p>
|
|
22
|
+
</div>
|
|
23
|
+
<template v-if="$slots.menu">
|
|
24
|
+
<button type="button" class="oc-media-row__menu" :popovertarget="menuId" aria-label="More actions">⋮</button>
|
|
25
|
+
<div :id="menuId" class="oc-dropdown-menu" popover><slot name="menu" /></div>
|
|
26
|
+
</template>
|
|
27
|
+
</li>
|
|
28
|
+
</template>
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcMediaThumb.vue | Map: props -> <a>/<div> -> picture -> badge -> duration -> progress -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* 16:9 video thumbnail (same markup as snippets/media-thumb.html). href renders an <a>.
|
|
5
|
-
* Props: src, alt, art (placeholder variant 1-6), duration, badge, live, progress (0-100), watched, ratio (video|square|portrait).
|
|
6
|
-
* Default slot: extra overlay content. Classes: .oc-media-thumb(--square/--portrait, .is-watched), __badge(--live), __duration, __progress
|
|
7
|
-
*/
|
|
8
|
-
withDefaults(
|
|
9
|
-
defineProps<{
|
|
10
|
-
src?: string
|
|
11
|
-
alt?: string
|
|
12
|
-
art?: number
|
|
13
|
-
href?: string
|
|
14
|
-
duration?: string
|
|
15
|
-
badge?: string
|
|
16
|
-
live?: boolean
|
|
17
|
-
progress?: number
|
|
18
|
-
watched?: boolean
|
|
19
|
-
ratio?: 'video' | 'square' | 'portrait'
|
|
20
|
-
}>(),
|
|
21
|
-
{ alt: '', ratio: 'video' },
|
|
22
|
-
)
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<template>
|
|
26
|
-
<component
|
|
27
|
-
:is="href ? 'a' : 'div'"
|
|
28
|
-
:href="href"
|
|
29
|
-
:class="['oc-media-thumb', ratio !== 'video' && `oc-media-thumb--${ratio}`, watched && 'is-watched']"
|
|
30
|
-
>
|
|
31
|
-
<img v-if="src" :src="src" :alt="alt" />
|
|
32
|
-
<div v-else :class="['oc-media-art', art && art > 1 && `oc-media-art--${art}`]" />
|
|
33
|
-
<span v-if="badge" :class="['oc-media-thumb__badge', live && 'oc-media-thumb__badge--live']">{{ badge }}</span>
|
|
34
|
-
<span v-if="duration" class="oc-media-thumb__duration">{{ duration }}</span>
|
|
35
|
-
<progress v-if="progress != null" class="oc-media-thumb__progress" :value="progress" max="100" :aria-label="`Watched ${progress}%`">{{ progress }}%</progress>
|
|
36
|
-
<slot />
|
|
37
|
-
</component>
|
|
38
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcMediaThumb.vue | Map: props -> <a>/<div> -> picture -> badge -> duration -> progress -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* 16:9 video thumbnail (same markup as snippets/media-thumb.html). href renders an <a>.
|
|
5
|
+
* Props: src, alt, art (placeholder variant 1-6), duration, badge, live, progress (0-100), watched, ratio (video|square|portrait).
|
|
6
|
+
* Default slot: extra overlay content. Classes: .oc-media-thumb(--square/--portrait, .is-watched), __badge(--live), __duration, __progress
|
|
7
|
+
*/
|
|
8
|
+
withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
src?: string
|
|
11
|
+
alt?: string
|
|
12
|
+
art?: number
|
|
13
|
+
href?: string
|
|
14
|
+
duration?: string
|
|
15
|
+
badge?: string
|
|
16
|
+
live?: boolean
|
|
17
|
+
progress?: number
|
|
18
|
+
watched?: boolean
|
|
19
|
+
ratio?: 'video' | 'square' | 'portrait'
|
|
20
|
+
}>(),
|
|
21
|
+
{ alt: '', ratio: 'video' },
|
|
22
|
+
)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<component
|
|
27
|
+
:is="href ? 'a' : 'div'"
|
|
28
|
+
:href="href"
|
|
29
|
+
:class="['oc-media-thumb', ratio !== 'video' && `oc-media-thumb--${ratio}`, watched && 'is-watched']"
|
|
30
|
+
>
|
|
31
|
+
<img v-if="src" :src="src" :alt="alt" />
|
|
32
|
+
<div v-else :class="['oc-media-art', art && art > 1 && `oc-media-art--${art}`]" />
|
|
33
|
+
<span v-if="badge" :class="['oc-media-thumb__badge', live && 'oc-media-thumb__badge--live']">{{ badge }}</span>
|
|
34
|
+
<span v-if="duration" class="oc-media-thumb__duration">{{ duration }}</span>
|
|
35
|
+
<progress v-if="progress != null" class="oc-media-thumb__progress" :value="progress" max="100" :aria-label="`Watched ${progress}%`">{{ progress }}%</progress>
|
|
36
|
+
<slot />
|
|
37
|
+
</component>
|
|
38
|
+
</template>
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
<!-- File: packages/vue/src/components/OcMessage.vue | Map: types + icons -> props -> article (avatar, bubble: sender, quote, text, attachments, meta; actions) -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
import type { AttachmentFile } from './OcAttachment.vue'
|
|
4
|
-
|
|
5
|
-
/** File chip (default), image preview or square thumbnail inside a message. */
|
|
6
|
-
export type MessageAttachment =
|
|
7
|
-
| (AttachmentFile & { kind?: 'file' })
|
|
8
|
-
| { kind: 'image'; href?: string; src?: string; alt: string; art?: number; caption?: string }
|
|
9
|
-
| { kind: 'thumb'; src?: string; alt: string; art?: number }
|
|
10
|
-
export interface MessageAction { name: string; label: string; icon?: 'reply' | 'delete'; destructive?: boolean }
|
|
11
|
-
export interface MessageData {
|
|
12
|
-
direction?: 'in' | 'out'
|
|
13
|
-
sender?: string
|
|
14
|
-
avatar?: string
|
|
15
|
-
time?: string
|
|
16
|
-
datetime?: string
|
|
17
|
-
status?: 'sent' | 'read' | 'failed'
|
|
18
|
-
statusLabel?: string
|
|
19
|
-
statusIcon?: boolean
|
|
20
|
-
continued?: boolean
|
|
21
|
-
quote?: { href?: string; sender: string; text: string }
|
|
22
|
-
/** Plain text: one paragraph. */
|
|
23
|
-
text?: string
|
|
24
|
-
/** Trusted markup (p, ul, ol, a, strong, code); escape user text on the server. */
|
|
25
|
-
html?: string
|
|
26
|
-
attachments?: MessageAttachment[]
|
|
27
|
-
actions?: MessageAction[]
|
|
28
|
-
label?: string
|
|
29
|
-
id?: string
|
|
30
|
-
}
|
|
31
|
-
export const MESSAGE_ICONS: Record<string, string> = {
|
|
32
|
-
reply: 'M9 14 4 9l5-5M4 9h10a6 6 0 0 1 6 6v4',
|
|
33
|
-
delete: 'M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6',
|
|
34
|
-
}
|
|
35
|
-
</script>
|
|
36
|
-
|
|
37
|
-
<script setup lang="ts">
|
|
38
|
-
/**
|
|
39
|
-
* Chat bubble (same markup as snippets/message.html): incoming / outgoing, sender, avatar, time, read receipt, reply quote,
|
|
40
|
-
* attachments and hover actions. Slot default = extra text content. Emits: action(name).
|
|
41
|
-
* Classes: .oc-message(--out/--continued) __avatar __bubble __sender __quote __text __attachments __meta __status(--read/--failed) __actions __action
|
|
42
|
-
*/
|
|
43
|
-
import { computed } from 'vue'
|
|
44
|
-
import OcAttachment from './OcAttachment.vue'
|
|
45
|
-
import OcAttachmentImage from './OcAttachmentImage.vue'
|
|
46
|
-
import OcAttachmentThumb from './OcAttachmentThumb.vue'
|
|
47
|
-
|
|
48
|
-
const props = withDefaults(defineProps<MessageData>(), { direction: 'in' })
|
|
49
|
-
const emit = defineEmits<{ action: [name: string] }>()
|
|
50
|
-
const STATUS = { sent: 'Sent', read: 'Read', failed: 'Failed' }
|
|
51
|
-
const thumbsOnly = computed(() => !!props.attachments?.length && props.attachments.every((a) => a.kind === 'thumb'))
|
|
52
|
-
// Props of one attachment without its `kind`.
|
|
53
|
-
type ImageAttachment = Extract<MessageAttachment, { kind: 'image' }>
|
|
54
|
-
type ThumbAttachment = Extract<MessageAttachment, { kind: 'thumb' }>
|
|
55
|
-
const strip = (a: MessageAttachment) => {
|
|
56
|
-
const { kind: _k, ...p } = a
|
|
57
|
-
return p
|
|
58
|
-
}
|
|
59
|
-
const image = (a: MessageAttachment) => strip(a) as Omit<ImageAttachment, 'kind'>
|
|
60
|
-
const thumb = (a: MessageAttachment) => strip(a) as Omit<ThumbAttachment, 'kind'>
|
|
61
|
-
const file = (a: MessageAttachment) => strip(a) as AttachmentFile
|
|
62
|
-
</script>
|
|
63
|
-
|
|
64
|
-
<template>
|
|
65
|
-
<article :id="id" :class="['oc-message', direction === 'out' && 'oc-message--out', continued && 'oc-message--continued']" :aria-label="label">
|
|
66
|
-
<span class="oc-avatar oc-message__avatar">{{ avatar }}</span>
|
|
67
|
-
<div class="oc-message__bubble">
|
|
68
|
-
<p v-if="sender && !continued" class="oc-message__sender">{{ sender }}</p>
|
|
69
|
-
<a v-if="quote" class="oc-message__quote" :href="quote.href"><strong>{{ quote.sender }}</strong><span>{{ quote.text }}</span></a>
|
|
70
|
-
<div v-if="html" class="oc-message__text" v-html="html"></div>
|
|
71
|
-
<div v-else-if="text || $slots.default" class="oc-message__text"><p v-if="text">{{ text }}</p><slot /></div>
|
|
72
|
-
<div v-if="attachments?.length" class="oc-message__attachments">
|
|
73
|
-
<div v-if="thumbsOnly" class="oc-attachment-list"><OcAttachmentThumb v-for="(a, i) in attachments" :key="i" v-bind="thumb(a)" as="span" /></div>
|
|
74
|
-
<template v-for="(a, i) in attachments" v-else :key="i">
|
|
75
|
-
<OcAttachmentImage v-if="a.kind === 'image'" v-bind="image(a)" />
|
|
76
|
-
<OcAttachmentThumb v-else-if="a.kind === 'thumb'" v-bind="thumb(a)" as="span" />
|
|
77
|
-
<OcAttachment v-else v-bind="file(a)" />
|
|
78
|
-
</template>
|
|
79
|
-
</div>
|
|
80
|
-
<p v-if="time || status" class="oc-message__meta">
|
|
81
|
-
<time v-if="time" :datetime="datetime">{{ time }}</time>
|
|
82
|
-
<span v-if="status" :class="['oc-message__status', status !== 'sent' && `oc-message__status--${status}`]"><svg v-if="statusIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12.5 7 17.5 18 6.5M13 16.5l1 1L22.5 9" /></svg>{{ statusLabel ?? STATUS[status] }}</span>
|
|
83
|
-
</p>
|
|
84
|
-
</div>
|
|
85
|
-
<div v-if="actions?.length" class="oc-message__actions">
|
|
86
|
-
<button v-for="a in actions" :key="a.name" type="button" :class="['oc-message__action', a.destructive && 'oc-message__action--destructive']" :aria-label="a.label" @click="emit('action', a.name)">
|
|
87
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path :d="MESSAGE_ICONS[a.icon ?? a.name] ?? MESSAGE_ICONS.reply" /></svg>
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
</article>
|
|
91
|
-
</template>
|
|
1
|
+
<!-- File: packages/vue/src/components/OcMessage.vue | Map: types + icons -> props -> article (avatar, bubble: sender, quote, text, attachments, meta; actions) -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import type { AttachmentFile } from './OcAttachment.vue'
|
|
4
|
+
|
|
5
|
+
/** File chip (default), image preview or square thumbnail inside a message. */
|
|
6
|
+
export type MessageAttachment =
|
|
7
|
+
| (AttachmentFile & { kind?: 'file' })
|
|
8
|
+
| { kind: 'image'; href?: string; src?: string; alt: string; art?: number; caption?: string }
|
|
9
|
+
| { kind: 'thumb'; src?: string; alt: string; art?: number }
|
|
10
|
+
export interface MessageAction { name: string; label: string; icon?: 'reply' | 'delete'; destructive?: boolean }
|
|
11
|
+
export interface MessageData {
|
|
12
|
+
direction?: 'in' | 'out'
|
|
13
|
+
sender?: string
|
|
14
|
+
avatar?: string
|
|
15
|
+
time?: string
|
|
16
|
+
datetime?: string
|
|
17
|
+
status?: 'sent' | 'read' | 'failed'
|
|
18
|
+
statusLabel?: string
|
|
19
|
+
statusIcon?: boolean
|
|
20
|
+
continued?: boolean
|
|
21
|
+
quote?: { href?: string; sender: string; text: string }
|
|
22
|
+
/** Plain text: one paragraph. */
|
|
23
|
+
text?: string
|
|
24
|
+
/** Trusted markup (p, ul, ol, a, strong, code); escape user text on the server. */
|
|
25
|
+
html?: string
|
|
26
|
+
attachments?: MessageAttachment[]
|
|
27
|
+
actions?: MessageAction[]
|
|
28
|
+
label?: string
|
|
29
|
+
id?: string
|
|
30
|
+
}
|
|
31
|
+
export const MESSAGE_ICONS: Record<string, string> = {
|
|
32
|
+
reply: 'M9 14 4 9l5-5M4 9h10a6 6 0 0 1 6 6v4',
|
|
33
|
+
delete: 'M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6',
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
/**
|
|
39
|
+
* Chat bubble (same markup as snippets/message.html): incoming / outgoing, sender, avatar, time, read receipt, reply quote,
|
|
40
|
+
* attachments and hover actions. Slot default = extra text content. Emits: action(name).
|
|
41
|
+
* Classes: .oc-message(--out/--continued) __avatar __bubble __sender __quote __text __attachments __meta __status(--read/--failed) __actions __action
|
|
42
|
+
*/
|
|
43
|
+
import { computed } from 'vue'
|
|
44
|
+
import OcAttachment from './OcAttachment.vue'
|
|
45
|
+
import OcAttachmentImage from './OcAttachmentImage.vue'
|
|
46
|
+
import OcAttachmentThumb from './OcAttachmentThumb.vue'
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<MessageData>(), { direction: 'in' })
|
|
49
|
+
const emit = defineEmits<{ action: [name: string] }>()
|
|
50
|
+
const STATUS = { sent: 'Sent', read: 'Read', failed: 'Failed' }
|
|
51
|
+
const thumbsOnly = computed(() => !!props.attachments?.length && props.attachments.every((a) => a.kind === 'thumb'))
|
|
52
|
+
// Props of one attachment without its `kind`.
|
|
53
|
+
type ImageAttachment = Extract<MessageAttachment, { kind: 'image' }>
|
|
54
|
+
type ThumbAttachment = Extract<MessageAttachment, { kind: 'thumb' }>
|
|
55
|
+
const strip = (a: MessageAttachment) => {
|
|
56
|
+
const { kind: _k, ...p } = a
|
|
57
|
+
return p
|
|
58
|
+
}
|
|
59
|
+
const image = (a: MessageAttachment) => strip(a) as Omit<ImageAttachment, 'kind'>
|
|
60
|
+
const thumb = (a: MessageAttachment) => strip(a) as Omit<ThumbAttachment, 'kind'>
|
|
61
|
+
const file = (a: MessageAttachment) => strip(a) as AttachmentFile
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<article :id="id" :class="['oc-message', direction === 'out' && 'oc-message--out', continued && 'oc-message--continued']" :aria-label="label">
|
|
66
|
+
<span class="oc-avatar oc-message__avatar">{{ avatar }}</span>
|
|
67
|
+
<div class="oc-message__bubble">
|
|
68
|
+
<p v-if="sender && !continued" class="oc-message__sender">{{ sender }}</p>
|
|
69
|
+
<a v-if="quote" class="oc-message__quote" :href="quote.href"><strong>{{ quote.sender }}</strong><span>{{ quote.text }}</span></a>
|
|
70
|
+
<div v-if="html" class="oc-message__text" v-html="html"></div>
|
|
71
|
+
<div v-else-if="text || $slots.default" class="oc-message__text"><p v-if="text">{{ text }}</p><slot /></div>
|
|
72
|
+
<div v-if="attachments?.length" class="oc-message__attachments">
|
|
73
|
+
<div v-if="thumbsOnly" class="oc-attachment-list"><OcAttachmentThumb v-for="(a, i) in attachments" :key="i" v-bind="thumb(a)" as="span" /></div>
|
|
74
|
+
<template v-for="(a, i) in attachments" v-else :key="i">
|
|
75
|
+
<OcAttachmentImage v-if="a.kind === 'image'" v-bind="image(a)" />
|
|
76
|
+
<OcAttachmentThumb v-else-if="a.kind === 'thumb'" v-bind="thumb(a)" as="span" />
|
|
77
|
+
<OcAttachment v-else v-bind="file(a)" />
|
|
78
|
+
</template>
|
|
79
|
+
</div>
|
|
80
|
+
<p v-if="time || status" class="oc-message__meta">
|
|
81
|
+
<time v-if="time" :datetime="datetime">{{ time }}</time>
|
|
82
|
+
<span v-if="status" :class="['oc-message__status', status !== 'sent' && `oc-message__status--${status}`]"><svg v-if="statusIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12.5 7 17.5 18 6.5M13 16.5l1 1L22.5 9" /></svg>{{ statusLabel ?? STATUS[status] }}</span>
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
<div v-if="actions?.length" class="oc-message__actions">
|
|
86
|
+
<button v-for="a in actions" :key="a.name" type="button" :class="['oc-message__action', a.destructive && 'oc-message__action--destructive']" :aria-label="a.label" @click="emit('action', a.name)">
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path :d="MESSAGE_ICONS[a.icon ?? a.name] ?? MESSAGE_ICONS.reply" /></svg>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</article>
|
|
91
|
+
</template>
|