@birdycom/omniaui-vue 0.12.6 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/blocks/OcBlockContactForm.vue.d.ts +71 -0
  2. package/dist/blocks/OcBlockProfileHeader.vue.d.ts +1 -1
  3. package/dist/components/OcBrandMark.vue.d.ts +23 -0
  4. package/dist/components/OcButton.vue.d.ts +3 -3
  5. package/dist/components/OcCallout.vue.d.ts +14 -0
  6. package/dist/components/OcDropdownItem.vue.d.ts +8 -6
  7. package/dist/components/OcFeatureCard.vue.d.ts +23 -0
  8. package/dist/components/OcGroup.vue.d.ts +24 -0
  9. package/dist/components/OcLanguageSelect.vue.d.ts +2 -0
  10. package/dist/components/OcLinkCard.vue.d.ts +22 -0
  11. package/dist/components/OcMenuStatic.vue.d.ts +16 -0
  12. package/dist/components/OcMeterRow.vue.d.ts +3 -0
  13. package/dist/components/OcPanel.vue.d.ts +16 -0
  14. package/dist/components/OcParticleField.vue.d.ts +8 -0
  15. package/dist/components/OcReading.vue.d.ts +16 -0
  16. package/dist/components/OcResult.vue.d.ts +23 -0
  17. package/dist/index.d.ts +13 -1
  18. package/dist/omniaui-vue.js +3516 -2853
  19. package/dist/omniaui-vue.js.map +1 -1
  20. package/manifest.json +66 -0
  21. package/package.json +1 -1
  22. package/src/blocks/OcBlockContactForm.vue +146 -0
  23. package/src/components/OcBrandMark.vue +15 -0
  24. package/src/components/OcButton.vue +2 -2
  25. package/src/components/OcCallout.vue +11 -0
  26. package/src/components/OcDropdownItem.vue +14 -4
  27. package/src/components/OcFeatureCard.vue +15 -0
  28. package/src/components/OcGroup.vue +16 -0
  29. package/src/components/OcLanguageSelect.vue +3 -1
  30. package/src/components/OcLinkCard.vue +16 -0
  31. package/src/components/OcMenuStatic.vue +9 -0
  32. package/src/components/OcMeterRow.vue +3 -2
  33. package/src/components/OcPanel.vue +9 -0
  34. package/src/components/OcParticleField.vue +23 -0
  35. package/src/components/OcReading.vue +9 -0
  36. package/src/components/OcResult.vue +16 -0
  37. package/src/index.ts +34 -0
package/manifest.json CHANGED
@@ -114,6 +114,12 @@
114
114
  "file": "src/components/OcBarcodeInput.vue",
115
115
  "snippet": "@birdycom/omniaui/snippets/barcode-input.html"
116
116
  },
117
+ {
118
+ "name": "OcBrandMark",
119
+ "kind": "component",
120
+ "file": "src/components/OcBrandMark.vue",
121
+ "snippet": "@birdycom/omniaui/snippets/brand-mark.html"
122
+ },
117
123
  {
118
124
  "name": "OcBreadcrumb",
119
125
  "kind": "component",
@@ -150,6 +156,12 @@
150
156
  "file": "src/components/OcButtonGroupText.vue",
151
157
  "snippet": null
152
158
  },
159
+ {
160
+ "name": "OcCallout",
161
+ "kind": "component",
162
+ "file": "src/components/OcCallout.vue",
163
+ "snippet": "@birdycom/omniaui/snippets/callout.html"
164
+ },
153
165
  {
154
166
  "name": "OcCard",
155
167
  "kind": "component",
@@ -324,6 +336,12 @@
324
336
  "file": "src/components/OcFactList.vue",
325
337
  "snippet": "@birdycom/omniaui/snippets/fact-list.html"
326
338
  },
339
+ {
340
+ "name": "OcFeatureCard",
341
+ "kind": "component",
342
+ "file": "src/components/OcFeatureCard.vue",
343
+ "snippet": "@birdycom/omniaui/snippets/feature-card.html"
344
+ },
327
345
  {
328
346
  "name": "OcFilterCounts",
329
347
  "kind": "component",
@@ -378,6 +396,12 @@
378
396
  "file": "src/components/OcGreeting.vue",
379
397
  "snippet": "@birdycom/omniaui/snippets/greeting.html"
380
398
  },
399
+ {
400
+ "name": "OcGroup",
401
+ "kind": "component",
402
+ "file": "src/components/OcGroup.vue",
403
+ "snippet": "@birdycom/omniaui/snippets/group.html"
404
+ },
381
405
  {
382
406
  "name": "OcHeading",
383
407
  "kind": "component",
@@ -474,6 +498,12 @@
474
498
  "file": "src/components/OcLinkArrow.vue",
475
499
  "snippet": "@birdycom/omniaui/snippets/link-arrow.html"
476
500
  },
501
+ {
502
+ "name": "OcLinkCard",
503
+ "kind": "component",
504
+ "file": "src/components/OcLinkCard.vue",
505
+ "snippet": null
506
+ },
477
507
  {
478
508
  "name": "OcLinkRow",
479
509
  "kind": "component",
@@ -516,6 +546,12 @@
516
546
  "file": "src/components/OcMediaThumb.vue",
517
547
  "snippet": "@birdycom/omniaui/snippets/media-thumb.html"
518
548
  },
549
+ {
550
+ "name": "OcMenuStatic",
551
+ "kind": "component",
552
+ "file": "src/components/OcMenuStatic.vue",
553
+ "snippet": "@birdycom/omniaui/snippets/menu-static.html"
554
+ },
519
555
  {
520
556
  "name": "OcMessage",
521
557
  "kind": "component",
@@ -594,6 +630,18 @@
594
630
  "file": "src/components/OcPagination.vue",
595
631
  "snippet": "@birdycom/omniaui/snippets/pagination.html"
596
632
  },
633
+ {
634
+ "name": "OcPanel",
635
+ "kind": "component",
636
+ "file": "src/components/OcPanel.vue",
637
+ "snippet": "@birdycom/omniaui/snippets/panel.html"
638
+ },
639
+ {
640
+ "name": "OcParticleField",
641
+ "kind": "component",
642
+ "file": "src/components/OcParticleField.vue",
643
+ "snippet": "@birdycom/omniaui/snippets/particle-field.html"
644
+ },
597
645
  {
598
646
  "name": "OcPasswordInput",
599
647
  "kind": "component",
@@ -684,12 +732,24 @@
684
732
  "file": "src/components/OcRatingInput.vue",
685
733
  "snippet": "@birdycom/omniaui/snippets/rating.html"
686
734
  },
735
+ {
736
+ "name": "OcReading",
737
+ "kind": "component",
738
+ "file": "src/components/OcReading.vue",
739
+ "snippet": "@birdycom/omniaui/snippets/reading.html"
740
+ },
687
741
  {
688
742
  "name": "OcRecoveryCodes",
689
743
  "kind": "component",
690
744
  "file": "src/components/OcRecoveryCodes.vue",
691
745
  "snippet": "@birdycom/omniaui/snippets/recovery-codes.html"
692
746
  },
747
+ {
748
+ "name": "OcResult",
749
+ "kind": "component",
750
+ "file": "src/components/OcResult.vue",
751
+ "snippet": "@birdycom/omniaui/snippets/result.html"
752
+ },
693
753
  {
694
754
  "name": "OcRibbon",
695
755
  "kind": "component",
@@ -1032,6 +1092,12 @@
1032
1092
  "file": "src/blocks/OcBlockAuthSplit.vue",
1033
1093
  "snippet": "@birdycom/omniaui-blocks/blocks/auth-split.html"
1034
1094
  },
1095
+ {
1096
+ "name": "OcBlockContactForm",
1097
+ "kind": "block",
1098
+ "file": "src/blocks/OcBlockContactForm.vue",
1099
+ "snippet": "@birdycom/omniaui-blocks/blocks/contact-form.html"
1100
+ },
1035
1101
  {
1036
1102
  "name": "OcBlockContactGrid",
1037
1103
  "kind": "block",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@birdycom/omniaui-vue",
3
- "version": "0.12.6",
3
+ "version": "0.14.0",
4
4
  "description": "OmniaUI for Vue 3: one single-file component per OmniaUI component and block, rendering the exact markup of the CSS packages.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,146 @@
1
+ <!-- File: packages/vue/src/blocks/OcBlockContactForm.vue | Map: props -> panel (result | title, intro, alert, form: honeypot, fields, actions) -> cards -->
2
+ <script setup lang="ts">
3
+ /**
4
+ * Contact form in a panel next to a column of contact cards. Server errors come in as `errors` (field name -> text,
5
+ * `form` = message above the form); `sent` shows the result, `busy` sets aria-busy and disables the button.
6
+ * Slots: fields (own fields instead of name, e-mail, phone, subject, message), note, success, card-icon ({ card }).
7
+ * Emits submit (the native event; call preventDefault for Inertia) and again (back to the form).
8
+ * Classes: .oc-block-contact-form, -layout, -title, -intro, -cards; .oc-panel--glow, .oc-form--filled, .oc-result
9
+ */
10
+ import { computed } from 'vue'
11
+ import { useUid } from '../composables/useUid'
12
+
13
+ export interface ContactFormLine {
14
+ text: string
15
+ href?: string
16
+ }
17
+ export interface ContactFormCard {
18
+ title: string
19
+ lines: ContactFormLine[]
20
+ /** Webicon name (mdi:email-outline …), drawn with <iconify-icon>. */
21
+ icon?: string
22
+ }
23
+ export interface ContactFormSubject {
24
+ value: string
25
+ label: string
26
+ }
27
+ const props = withDefaults(
28
+ defineProps<{
29
+ title?: string
30
+ intro?: string
31
+ action?: string
32
+ method?: string
33
+ subjects?: ContactFormSubject[]
34
+ values?: Record<string, string>
35
+ errors?: Record<string, string>
36
+ sent?: boolean
37
+ busy?: boolean
38
+ submitLabel?: string
39
+ busyLabel?: string
40
+ note?: string
41
+ successTitle?: string
42
+ successText?: string
43
+ againLabel?: string
44
+ errorText?: string
45
+ cards?: ContactFormCard[]
46
+ }>(),
47
+ {
48
+ title: 'Send us a message',
49
+ method: 'post',
50
+ subjects: () => [],
51
+ values: () => ({}),
52
+ errors: () => ({}),
53
+ submitLabel: 'Send message',
54
+ busyLabel: 'Sending…',
55
+ successTitle: 'Thank you, your message was sent',
56
+ againLabel: 'Send another message',
57
+ errorText: 'Please check the fields in red.',
58
+ cards: () => [],
59
+ },
60
+ )
61
+ const emit = defineEmits<{ submit: [event: Event]; again: [] }>()
62
+ const uid = useUid('oc-contact')
63
+ const fid = (name: string) => `${uid}-${name}`
64
+ const invalid = (name: string) => (props.errors[name] ? { 'aria-invalid': true as const, 'aria-describedby': `err-${name}` } : {})
65
+ const alertText = computed(() => props.errors.form ?? (Object.keys(props.errors).some((k) => k !== 'form') ? props.errorText : null))
66
+ </script>
67
+
68
+ <template>
69
+ <section class="oc-block-contact-form" aria-label="Contact">
70
+ <div class="oc-block-contact-form-layout">
71
+ <div :id="uid" class="oc-panel oc-panel--glow">
72
+ <div v-if="sent" class="oc-result" role="status" tabindex="-1">
73
+ <slot name="success">
74
+ <span class="oc-icon-tile oc-icon-tile--sm oc-accent--green" aria-hidden="true"><iconify-icon icon="mdi:check" /></span>
75
+ <h2 class="oc-result__title">{{ successTitle }}</h2>
76
+ <p v-if="successText" class="oc-result__text">{{ successText }}</p>
77
+ <div class="oc-result__actions">
78
+ <button type="button" class="oc-btn oc-btn--outline" @click="emit('again')">{{ againLabel }}</button>
79
+ </div>
80
+ </slot>
81
+ </div>
82
+ <div v-else>
83
+ <h2 v-if="title" class="oc-block-contact-form-title">{{ title }}</h2>
84
+ <p v-if="intro" class="oc-block-contact-form-intro">{{ intro }}</p>
85
+ <p v-if="alertText" class="oc-alert oc-alert--danger" role="alert">{{ alertText }}</p>
86
+ <form class="oc-form oc-form--filled" :action="action" :method="method" novalidate :aria-busy="busy || undefined" @submit="emit('submit', $event)">
87
+ <div class="oc-honeypot" aria-hidden="true">
88
+ <label :for="fid('website')">Leave this field empty</label>
89
+ <input :id="fid('website')" type="text" name="website" tabindex="-1" autocomplete="off" />
90
+ </div>
91
+ <slot name="fields">
92
+ <div class="oc-field-group oc-field-group--row">
93
+ <div class="oc-field">
94
+ <label class="oc-field-label" :for="fid('name')">Name</label>
95
+ <input :id="fid('name')" class="oc-input" type="text" name="name" required maxlength="100" autocomplete="name" :value="values.name" v-bind="invalid('name')" />
96
+ <p v-if="errors.name" id="err-name" class="oc-field-error">{{ errors.name }}</p>
97
+ </div>
98
+ <div class="oc-field">
99
+ <label class="oc-field-label" :for="fid('email')">E-mail</label>
100
+ <input :id="fid('email')" class="oc-input" type="email" name="email" required maxlength="254" autocomplete="email" :value="values.email" v-bind="invalid('email')" />
101
+ <p v-if="errors.email" id="err-email" class="oc-field-error">{{ errors.email }}</p>
102
+ </div>
103
+ </div>
104
+ <div :class="['oc-field-group', subjects.length && 'oc-field-group--row']">
105
+ <div class="oc-field">
106
+ <label class="oc-field-label" :for="fid('phone')">Phone <span class="oc-field-label__hint">(optional)</span></label>
107
+ <input :id="fid('phone')" class="oc-input" type="tel" name="phone" maxlength="40" autocomplete="tel" inputmode="tel" :value="values.phone" v-bind="invalid('phone')" />
108
+ <p v-if="errors.phone" id="err-phone" class="oc-field-error">{{ errors.phone }}</p>
109
+ </div>
110
+ <div v-if="subjects.length" class="oc-field">
111
+ <label class="oc-field-label" :for="fid('subject')">What is it about?</label>
112
+ <select :id="fid('subject')" class="oc-select" name="subject" required v-bind="invalid('subject')">
113
+ <option value="" :selected="!values.subject">Choose a subject</option>
114
+ <option v-for="s in subjects" :key="s.value" :value="s.value" :selected="values.subject === s.value">{{ s.label }}</option>
115
+ </select>
116
+ <p v-if="errors.subject" id="err-subject" class="oc-field-error">{{ errors.subject }}</p>
117
+ </div>
118
+ </div>
119
+ <div class="oc-field">
120
+ <label class="oc-field-label" :for="fid('message')">Your message</label>
121
+ <textarea :id="fid('message')" class="oc-input oc-textarea" name="message" rows="6" required maxlength="5000" :value="values.message" v-bind="invalid('message')" />
122
+ <p v-if="errors.message" id="err-message" class="oc-field-error">{{ errors.message }}</p>
123
+ </div>
124
+ </slot>
125
+ <div class="oc-form-actions">
126
+ <button class="oc-btn oc-btn--lg" type="submit" :disabled="busy">{{ busy ? busyLabel : submitLabel }}</button>
127
+ <p v-if="note || $slots.note" class="oc-form-note"><slot name="note">{{ note }}</slot></p>
128
+ </div>
129
+ </form>
130
+ </div>
131
+ </div>
132
+ <div v-if="cards.length" class="oc-block-contact-form-cards">
133
+ <article v-for="(card, i) in cards" :key="i" class="oc-block-contact-card">
134
+ <span v-if="card.icon || $slots['card-icon']" class="oc-icon-tile oc-icon-tile--sm" aria-hidden="true">
135
+ <slot name="card-icon" :card="card"><iconify-icon :icon="card.icon" /></slot>
136
+ </span>
137
+ <h3 class="oc-block-contact-title">{{ card.title }}</h3>
138
+ <template v-for="(line, j) in card.lines" :key="j">
139
+ <a v-if="line.href" class="oc-block-contact-line" :href="line.href">{{ line.text }}</a>
140
+ <p v-else class="oc-block-contact-line">{{ line.text }}</p>
141
+ </template>
142
+ </article>
143
+ </div>
144
+ </div>
145
+ </section>
146
+ </template>
@@ -0,0 +1,15 @@
1
+ <!-- File: packages/vue/src/components/OcBrandMark.vue | Map: link -> icon (slot) -> word (name + accent) -> tagline -->
2
+ <script setup lang="ts">
3
+ /** Logo from an icon and text (snippets/brand-mark.html). Slot icon: an <svg class="oc-brand-mark__icon"> or <img>. */
4
+ withDefaults(defineProps<{ name: string; accent?: string; tagline?: string; href?: string; size?: 'sm' | 'md' | 'lg' }>(), { href: '/', size: 'md' })
5
+ </script>
6
+
7
+ <template>
8
+ <a :class="['oc-brand-mark', size !== 'md' && `oc-brand-mark--${size}`]" :href="href">
9
+ <slot name="icon" />
10
+ <span class="oc-brand-mark__text">
11
+ <span class="oc-brand-mark__word">{{ name }}<span v-if="accent" class="oc-brand-mark__accent">{{ accent }}</span></span>
12
+ <span v-if="tagline" class="oc-brand-mark__tagline">{{ tagline }}</span>
13
+ </span>
14
+ </a>
15
+ </template>
@@ -1,14 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * Button. Renders <a> when `href` is set, otherwise <button>.
4
- * Classes: .oc-btn + --secondary/--outline/--ghost/--link/--destructive/--success/--gradient/--glass,
4
+ * Classes: .oc-btn + --secondary/--outline/--ghost/--link/--destructive/--success/--gradient/--glass/--neon,
5
5
  * --soft-primary/-success/-warning/-destructive/-info (row actions), --plain (icon only, no surface),
6
6
  * --xs/--sm/--lg, --icon, --pill, --inline (link button inside running text). `loading` sets aria-busy (spinner).
7
7
  */
8
8
  withDefaults(
9
9
  defineProps<{
10
10
  variant?:
11
- | 'default' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive' | 'success' | 'gradient' | 'glass'
11
+ | 'default' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive' | 'success' | 'gradient' | 'glass' | 'neon'
12
12
  | 'soft-primary' | 'soft-success' | 'soft-warning' | 'soft-destructive' | 'soft-info' | 'plain'
13
13
  size?: 'xs' | 'sm' | 'md' | 'lg'
14
14
  icon?: boolean
@@ -0,0 +1,11 @@
1
+ <!-- File: packages/vue/src/components/OcCallout.vue | Map: card row -> text (default slot) -> action (slot) -->
2
+ <script setup lang="ts">
3
+ /** Small card with a text on the left (default slot) and an action on the right (slot `action`). */
4
+ </script>
5
+
6
+ <template>
7
+ <div class="oc-callout">
8
+ <div class="oc-callout__text"><slot /></div>
9
+ <div v-if="$slots.action" class="oc-callout__action"><slot name="action" /></div>
10
+ </div>
11
+ </template>
@@ -1,9 +1,19 @@
1
1
  <script setup lang="ts">
2
- /** Menu item: <a> with `href`, otherwise <button>. */
3
- defineProps<{ href?: string }>()
2
+ /** Menu item: <a> with `href`, otherwise <button>. checked (true/false) adds a check column, hint a muted text on the right. */
3
+ withDefaults(defineProps<{ href?: string; checked?: boolean; hint?: string }>(), { checked: undefined })
4
4
  </script>
5
5
 
6
6
  <template>
7
- <a v-if="href" class="oc-dropdown-item" :href="href"><slot /></a>
8
- <button v-else class="oc-dropdown-item" type="button"><slot /></button>
7
+ <component
8
+ :is="href ? 'a' : 'button'"
9
+ class="oc-dropdown-item"
10
+ :href="href"
11
+ :type="href ? undefined : 'button'"
12
+ :role="checked !== undefined ? 'menuitemcheckbox' : undefined"
13
+ :aria-checked="checked !== undefined ? String(checked) : undefined"
14
+ >
15
+ <span v-if="checked !== undefined" class="oc-dropdown-check" aria-hidden="true"></span>
16
+ <slot />
17
+ <span v-if="hint" class="oc-dropdown-hint">{{ hint }}</span>
18
+ </component>
9
19
  </template>
@@ -0,0 +1,15 @@
1
+ <!-- File: packages/vue/src/components/OcFeatureCard.vue | Map: tile (icon) -> title -> text (slot) -->
2
+ <script setup lang="ts">
3
+ /** Service or feature card (snippets/feature-card.html). icon = webicon name; slots: default (text), icon. */
4
+ withDefaults(defineProps<{ title: string; icon?: string; accent?: string; headingLevel?: 2 | 3 | 4 }>(), { headingLevel: 3 })
5
+ </script>
6
+
7
+ <template>
8
+ <article class="oc-feature-card">
9
+ <span v-if="icon || $slots.icon" :class="['oc-icon-tile oc-icon-tile--sm', accent && `oc-accent--${accent}`]" aria-hidden="true">
10
+ <slot name="icon"><iconify-icon :icon="icon" /></slot>
11
+ </span>
12
+ <component :is="`h${headingLevel}`" class="oc-feature-card__title">{{ title }}</component>
13
+ <p v-if="$slots.default" class="oc-feature-card__text"><slot /></p>
14
+ </article>
15
+ </template>
@@ -0,0 +1,16 @@
1
+ <!-- File: packages/vue/src/components/OcGroup.vue | Map: section -> head (title + aside) -> meta -> content (slot) -->
2
+ <script setup lang="ts">
3
+ /** One section in a panel: title, a status on the right (aside, asideTone), a muted line (meta) and the content. */
4
+ withDefaults(defineProps<{ title: string; aside?: string; asideTone?: 'busy' | 'ok' | 'warn' | 'error'; meta?: string; headingLevel?: 2 | 3 | 4 }>(), { headingLevel: 2 })
5
+ </script>
6
+
7
+ <template>
8
+ <section class="oc-group">
9
+ <header class="oc-group__head">
10
+ <component :is="`h${headingLevel}`" class="oc-group__title">{{ title }}</component>
11
+ <span v-if="aside || $slots.aside" class="oc-group__aside" :data-tone="asideTone"><slot name="aside">{{ aside }}</slot></span>
12
+ </header>
13
+ <p v-if="meta" class="oc-group__meta">{{ meta }}</p>
14
+ <slot />
15
+ </section>
16
+ </template>
@@ -18,6 +18,8 @@ defineProps<{
18
18
  label?: string
19
19
  currency?: boolean
20
20
  ghost?: boolean
21
+ /** Closed it shows only the code ("NL"), the value in upper case */
22
+ code?: boolean
21
23
  /** Text icon, e.g. "€" (default: globe, or € with `currency`) */
22
24
  icon?: string
23
25
  }>()
@@ -26,7 +28,7 @@ const { root, control } = useAttrsSplit()
26
28
  </script>
27
29
 
28
30
  <template>
29
- <label v-bind="root" :class="['oc-language-select', ghost && 'oc-language-select--ghost', currency && 'oc-language-select--currency']">
31
+ <label v-bind="root" :class="['oc-language-select', ghost && 'oc-language-select--ghost', currency && 'oc-language-select--currency', code && 'oc-language-select--code']" :data-code="code ? String(model ?? options[0]?.value ?? '').toUpperCase() : undefined">
30
32
  <span class="oc-language-select-icon" aria-hidden="true">
31
33
  <slot name="icon">
32
34
  <template v-if="icon || currency">{{ icon ?? '€' }}</template>
@@ -0,0 +1,16 @@
1
+ <!-- File: packages/vue/src/components/OcLinkCard.vue | Map: link -> tile (icon) -> title -> text (slot) -> more -->
2
+ <script setup lang="ts">
3
+ /** A card that is a whole link (snippets/feature-card.html). icon = webicon name; slots: default (text), icon. */
4
+ defineProps<{ title: string; href: string; icon?: string; accent?: string; more?: string }>()
5
+ </script>
6
+
7
+ <template>
8
+ <a :class="['oc-link-card', accent && `oc-accent--${accent}`]" :href="href">
9
+ <span v-if="icon || $slots.icon" class="oc-icon-tile oc-icon-tile--sm" aria-hidden="true">
10
+ <slot name="icon"><iconify-icon :icon="icon" /></slot>
11
+ </span>
12
+ <span class="oc-link-card__title">{{ title }}</span>
13
+ <span v-if="$slots.default" class="oc-link-card__text"><slot /></span>
14
+ <span v-if="more" class="oc-link-card__more">{{ more }}</span>
15
+ </a>
16
+ </template>
@@ -0,0 +1,9 @@
1
+ <!-- File: packages/vue/src/components/OcMenuStatic.vue | Map: div.oc-dropdown-menu--static -> items (slot) -->
2
+ <script setup lang="ts">
3
+ /** The dropdown menu as a surface of its own (a tray menu). Items: OcDropdownItem (checked, hint), OcDropdownLabel. */
4
+ defineProps<{ flat?: boolean }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div role="menu" :class="['oc-dropdown-menu', 'oc-dropdown-menu--static', flat && 'oc-dropdown-menu--flat']"><slot /></div>
9
+ </template>
@@ -1,9 +1,9 @@
1
1
  <!-- File: packages/vue/src/components/OcMeterRow.vue | Map: props -> level from the percentage -> row markup -->
2
2
  <script setup lang="ts">
3
- /** Usage line: label, percentage and amount, a colored bar (warn from 75, critical from 90) and the reset line. */
3
+ /** Usage line: label, percentage and amount, a colored bar (warn from 75, critical from 90), a sub-line (sub, subPercent) and the reset line. */
4
4
  import { computed } from 'vue'
5
5
 
6
- const props = defineProps<{ label: string; percent: number; value?: string; reset?: string; level?: 'ok' | 'warn' | 'critical' }>()
6
+ const props = defineProps<{ label: string; percent: number; value?: string; reset?: string; level?: 'ok' | 'warn' | 'critical'; sub?: string; subPercent?: number; subLevel?: 'warn' | 'critical' }>()
7
7
  const pct = computed(() => Math.max(0, Math.min(100, Math.round(props.percent))))
8
8
  const tone = computed(() => props.level ?? (pct.value >= 90 ? 'critical' : pct.value >= 75 ? 'warn' : 'ok'))
9
9
  </script>
@@ -13,6 +13,7 @@ const tone = computed(() => props.level ?? (pct.value >= 90 ? 'critical' : pct.v
13
13
  <span class="oc-meter-row__label">{{ label }}</span>
14
14
  <span class="oc-meter-row__value"><strong>{{ pct }}%</strong><template v-if="value"> · {{ value }}</template></span>
15
15
  <span class="oc-meter-row__bar" role="meter" aria-valuemin="0" aria-valuemax="100" :aria-valuenow="pct" :aria-label="label"></span>
16
+ <span v-if="sub" class="oc-meter-row__sub">{{ sub }}<span v-if="subPercent != null" class="oc-meter-row__subbar" :data-level="subLevel" :style="{ '--oc-meter': `${Math.max(0, Math.min(100, subPercent))}%` }"></span></span>
16
17
  <span v-if="reset" class="oc-meter-row__reset">{{ reset }}</span>
17
18
  </div>
18
19
  </template>
@@ -0,0 +1,9 @@
1
+ <!-- File: packages/vue/src/components/OcPanel.vue | Map: glow -> classes -> panel with the slot -->
2
+ <script setup lang="ts">
3
+ /** Framed panel for a form, a call to action or a feature (same markup as snippets/panel.html). glow: start | end | both. */
4
+ defineProps<{ glow?: 'start' | 'end' | 'both' }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div :class="['oc-panel', (glow === 'start' || glow === 'end') && 'oc-panel--glow', glow === 'end' && 'oc-panel--glow-end', glow === 'both' && 'oc-panel--glow-both']"><slot /></div>
9
+ </template>
@@ -0,0 +1,23 @@
1
+ <!-- File: packages/vue/src/components/OcParticleField.vue | Map: canvas.oc-particles -> start on mount (particles.js) -> stop on unmount -->
2
+ <script setup lang="ts">
3
+ /** Background canvas with drifting points and lines; one still frame with reduced motion. local = fill the positioned parent. */
4
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
5
+ import { initParticles } from '../../../ui/src/particles.js'
6
+
7
+ const props = defineProps<{ count?: number; speed?: number; link?: number; local?: boolean }>()
8
+ const canvas = ref<HTMLCanvasElement | null>(null)
9
+ let stop = () => {}
10
+ const start = () => {
11
+ stop()
12
+ if (!canvas.value) return
13
+ const options = Object.fromEntries(Object.entries({ count: props.count, speed: props.speed, link: props.link }).filter(([, v]) => v != null))
14
+ stop = initParticles(canvas.value, options)
15
+ }
16
+ onMounted(start)
17
+ watch(() => [props.count, props.speed, props.link], start)
18
+ onBeforeUnmount(() => stop())
19
+ </script>
20
+
21
+ <template>
22
+ <canvas ref="canvas" aria-hidden="true" :class="['oc-particles', local && 'oc-particles--local']"></canvas>
23
+ </template>
@@ -0,0 +1,9 @@
1
+ <!-- File: packages/vue/src/components/OcReading.vue | Map: span.oc-reading (+ level) -> value (slot) -->
2
+ <script setup lang="ts">
3
+ /** A small measured value ("57 °C") in tabular numbers. level: cool (default), warm, hot. */
4
+ defineProps<{ level?: 'cool' | 'warm' | 'hot' }>()
5
+ </script>
6
+
7
+ <template>
8
+ <span :class="['oc-reading', level && level !== 'cool' && `oc-reading--${level}`]"><slot /></span>
9
+ </template>
@@ -0,0 +1,16 @@
1
+ <!-- File: packages/vue/src/components/OcResult.vue | Map: tile (icon) -> title -> text (slot) -> actions (slot) -->
2
+ <script setup lang="ts">
3
+ /** Outcome of an action in place of the form (snippets/result.html). icon = webicon name; slots: default (text), icon, actions. */
4
+ defineProps<{ title: string; icon?: string; accent?: string; center?: boolean }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div :class="['oc-result', center && 'oc-result--center']" role="status" tabindex="-1">
9
+ <span v-if="icon || $slots.icon" :class="['oc-icon-tile oc-icon-tile--sm', accent && `oc-accent--${accent}`]" aria-hidden="true">
10
+ <slot name="icon"><iconify-icon :icon="icon" /></slot>
11
+ </span>
12
+ <h2 class="oc-result__title">{{ title }}</h2>
13
+ <p v-if="$slots.default" class="oc-result__text"><slot /></p>
14
+ <div v-if="$slots.actions" class="oc-result__actions"><slot name="actions" /></div>
15
+ </div>
16
+ </template>