@ulu/frontend-vue 0.1.0-beta.26 → 0.1.0-beta.28

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.
@@ -0,0 +1,78 @@
1
+ <template>
2
+ <div class="menu-stack form-theme" :role="groupRole" :aria-labelledby="legendId">
3
+ <div v-if="legend" :id="legendId" class="hidden-visually">{{ legend }}</div>
4
+ <ul class="menu-stack__list">
5
+ <li class="menu-stack__item" v-for="option in options" :key="option.uid">
6
+ <div class="menu-stack__selectable">
7
+ <input
8
+ :type="type"
9
+ :id="getId(option)"
10
+ :name="name"
11
+ :value="option.uid"
12
+ :checked="isChecked(option)"
13
+ @change="handleChange(option, $event)"
14
+ >
15
+ <label :for="getId(option)">
16
+ <slot :option="option">
17
+ {{ option?.label || option?.title || option?.text }}
18
+ </slot>
19
+ </label>
20
+ </div>
21
+ </li>
22
+ </ul>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup>
27
+ import { computed } from 'vue';
28
+
29
+ const props = defineProps({
30
+ legend: String,
31
+ options: Array,
32
+ type: {
33
+ type: String,
34
+ default: 'checkbox',
35
+ },
36
+ modelValue: [String, Array],
37
+ });
38
+
39
+ const emit = defineEmits(['update:modelValue']);
40
+
41
+ const name = computed(() => props.legend ? props.legend.toLowerCase().replace(/\s+/g, '-') : `menu-${Math.random().toString(36).substring(7)}`);
42
+ const legendId = computed(() => name.value ? `${name.value}-legend` : null);
43
+ const groupRole = computed(() => props.type === 'radio' ? 'radiogroup' : 'group');
44
+
45
+ const getId = (option) => `${name.value}-${option.uid}`;
46
+
47
+ const isChecked = (option) => {
48
+ if (props.type === 'radio') {
49
+ return props.modelValue === option.uid;
50
+ }
51
+ if (Array.isArray(props.modelValue)) {
52
+ return props.modelValue.includes(option.uid);
53
+ }
54
+ if (props.type === 'checkbox') {
55
+ return option.checked || false;
56
+ }
57
+ return false;
58
+ };
59
+
60
+ const handleChange = (option, event) => {
61
+ if (props.type === 'radio') {
62
+ emit('update:modelValue', option.uid);
63
+ } else {
64
+ if (Array.isArray(props.modelValue)) {
65
+ const newValue = [...props.modelValue];
66
+ const index = newValue.indexOf(option.uid);
67
+ if (index > -1) {
68
+ newValue.splice(index, 1);
69
+ } else {
70
+ newValue.push(option.uid);
71
+ }
72
+ emit('update:modelValue', newValue);
73
+ } else {
74
+ option.checked = event.target.checked;
75
+ }
76
+ }
77
+ };
78
+ </script>
@@ -27,7 +27,7 @@ export { default as UluList } from './elements/UluList.vue';
27
27
  export { default as UluMain } from './elements/UluMain.vue';
28
28
  export { default as UluSpokeSpinner } from './elements/UluSpokeSpinner.vue';
29
29
  export { default as UluTag } from './elements/UluTag.vue';
30
- export { default as UluCheckboxMenu } from './forms/UluCheckboxMenu.vue';
30
+ export { default as UluSelectableMenu } from './forms/UluSelectableMenu.vue';
31
31
  export { default as UluFileDisplay } from './forms/UluFileDisplay.vue';
32
32
  export { default as UluFormFile } from './forms/UluFormFile.vue';
33
33
  export { default as UluFormMessage } from './forms/UluFormMessage.vue';
@@ -4,9 +4,10 @@
4
4
  aria-label="Breadcrumb"
5
5
  v-if="items.length"
6
6
  >
7
- <ul :class="classes.list">
7
+ <ol :class="classes.list">
8
8
  <li v-for="(item, index) in items" :key="index" :class="classes.item">
9
9
  <router-link
10
+ v-if="!item.current"
10
11
  :to="item.to"
11
12
  :class="classes.link"
12
13
  :aria-current="item.current ? 'page' : null"
@@ -15,6 +16,11 @@
15
16
  {{ item.title }}
16
17
  </slot>
17
18
  </router-link>
19
+ <span v-else :class="item.current">
20
+ <slot :item="item">
21
+ {{ item.title }}
22
+ </slot>
23
+ </span>
18
24
  <template v-if="index < items.length - 1">
19
25
  <slot name="separator">
20
26
  <UluIcon
@@ -24,7 +30,7 @@
24
30
  </slot>
25
31
  </template>
26
32
  </li>
27
- </ul>
33
+ </ol>
28
34
  </nav>
29
35
  </template>
30
36
 
@@ -63,6 +69,7 @@
63
69
  list: "breadcrumb__list",
64
70
  item: "breadcrumb__item",
65
71
  link: "breadcrumb__link",
72
+ current: "breadcrumb__current",
66
73
  separator: "breadcrumb__separator"
67
74
  })
68
75
  }
@@ -9,7 +9,7 @@
9
9
  <template #sidebar>
10
10
  <UluFacetsSearch v-model="searchValue" />
11
11
  <UluFacetsSort v-model="selectedSort" :sort-types="sortTypes" />
12
- <UluFacetsFilters :facets="facets" @facet-change="handleFacetChange" />
12
+ <UluFacetsFilterLists :facets="facets" @facet-change="handleFacetChange" />
13
13
  </template>
14
14
  <template #main>
15
15
  <UluFacetsResults :items="paginatedItems">
@@ -45,7 +45,7 @@
45
45
  useFacets,
46
46
  usePagination,
47
47
  UluFacetsSidebarLayout,
48
- UluFacetsFilters,
48
+ UluFacetsFilterLists,
49
49
  UluFacetsSort,
50
50
  UluFacetsSearch,
51
51
  UluFacetsResults
@@ -22,7 +22,9 @@
22
22
  <UluFacetsList
23
23
  :children="group.children.slice(0, maxVisible)"
24
24
  :groupUid="group.uid"
25
- :classFacet="classes.facet"
25
+ :groupName="group.name"
26
+ :type="group.multiple ? 'checkbox' : 'radio'"
27
+ :model-value="selectedUids(group)"
26
28
  @facet-change="emit('facet-change', $event)"
27
29
  />
28
30
  <UluCollapsibleRegion
@@ -40,7 +42,9 @@
40
42
  <UluFacetsList
41
43
  :children="group.children.slice(maxVisible)"
42
44
  :groupUid="group.uid"
43
- :classFacet="classes.facet"
45
+ :groupName="group.name"
46
+ :type="group.multiple ? 'checkbox' : 'radio'"
47
+ :model-value="selectedUids(group)"
44
48
  @facet-change="emit('facet-change', $event)"
45
49
  />
46
50
  </template>
@@ -70,4 +74,11 @@
70
74
  });
71
75
 
72
76
  const emit = defineEmits(['facet-change']);
73
- </script>
77
+
78
+ const selectedUids = (group) => {
79
+ if (group.multiple) {
80
+ return group.children.filter(c => c.selected).map(c => c.uid);
81
+ }
82
+ return group.children.find(c => c.selected)?.uid || '';
83
+ };
84
+ </script>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <div :class="classes.container">
3
+ <div v-for="group in facets" :key="group.uid" :class="classes.group">
4
+ <UluPopover
5
+ :classes="{
6
+ trigger: classes.trigger,
7
+ content: classes.content
8
+ }
9
+ ">
10
+ <template #trigger>
11
+ <span>{{ group.name }}: <strong>{{ selectedLabel(group) }}</strong></span>
12
+ <UluIcon :class="classes.triggerIcon" icon="fas fa-chevron-down" />
13
+ </template>
14
+ <template #content="{ close }">
15
+ <UluSelectableMenu
16
+ :legend="group.name"
17
+ :type="group.multiple ? 'checkbox' : 'radio'"
18
+ :options="getMenuOptions(group)"
19
+ :model-value="selectedUids(group)"
20
+ @update:model-value="onFilterChange(group, $event, close)"
21
+ />
22
+ </template>
23
+ </UluPopover>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import UluPopover from '../../../plugins/popovers/UluPopover.vue';
30
+ import UluSelectableMenu from '../../forms/UluSelectableMenu.vue';
31
+ import UluIcon from '../../elements/UluIcon.vue';
32
+
33
+ const props = defineProps({
34
+ facets: {
35
+ type: Array,
36
+ default: () => []
37
+ },
38
+ classes: {
39
+ type: Object,
40
+ default: () => ({
41
+ trigger: "button",
42
+ triggerIcon: "button__icon"
43
+ // content: null,
44
+ // container: null,
45
+ // group: null
46
+ })
47
+ }
48
+ });
49
+
50
+ const emit = defineEmits(['facet-change']);
51
+
52
+ const getMenuOptions = (group) => {
53
+ if (group.multiple) {
54
+ return group.children;
55
+ }
56
+ return [{ label: `All ${group.name}s`, uid: '' }, ...group.children];
57
+ };
58
+
59
+ const selectedUids = (group) => {
60
+ if (group.multiple) {
61
+ return group.children.filter(c => c.selected).map(c => c.uid);
62
+ }
63
+ return group.children.find(c => c.selected)?.uid || '';
64
+ };
65
+
66
+ const selectedLabel = (group) => {
67
+ const selectedItems = group.children.filter(c => c.selected);
68
+ const count = selectedItems.length;
69
+
70
+ if (count === 0) {
71
+ return 'All';
72
+ }
73
+
74
+ if (group.multiple) {
75
+ if (count === 1) {
76
+ return selectedItems[0].label;
77
+ }
78
+ return `${count} selected`;
79
+ }
80
+
81
+ return selectedItems[0].label;
82
+ };
83
+
84
+ function onFilterChange(group, value, closePopover) {
85
+ if (group.multiple) {
86
+ const selectedUids = new Set(value);
87
+ group.children.forEach(facet => {
88
+ const shouldBeSelected = selectedUids.has(facet.uid);
89
+ if (facet.selected !== shouldBeSelected) {
90
+ emit('facet-change', {
91
+ groupUid: group.uid,
92
+ facetUid: facet.uid,
93
+ selected: shouldBeSelected
94
+ });
95
+ }
96
+ });
97
+ } else {
98
+ const selectedUid = value;
99
+ group.children.forEach(facet => {
100
+ const shouldBeSelected = facet.uid === selectedUid;
101
+ if (facet.selected !== shouldBeSelected) {
102
+ emit('facet-change', {
103
+ groupUid: group.uid,
104
+ facetUid: facet.uid,
105
+ selected: shouldBeSelected
106
+ });
107
+ }
108
+ });
109
+ closePopover();
110
+ }
111
+ }
112
+ </script>
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div class="UluFacetsDropdownFilters">
3
+ <div
4
+ class="UluFacetsDropdownFilters__group"
5
+ v-for="group in facets"
6
+ :key="group.uid"
7
+ >
8
+ <label :for="`facet-dropdown-${group.uid}`" class="UluFacetsDropdownFilters__label">
9
+ {{ group.name }}
10
+ </label>
11
+ <select
12
+ :id="`facet-dropdown-${group.uid}`"
13
+ class="UluFacetsDropdownFilters__select"
14
+ @change="onFilterChange(group, $event)"
15
+ >
16
+ <option value="">All {{ group.name }}s</option>
17
+ <option
18
+ v-for="option in group.children"
19
+ :key="option.uid"
20
+ :value="option.uid"
21
+ :selected="option.selected"
22
+ >
23
+ {{ option.label }}
24
+ </option>
25
+ </select>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ const props = defineProps({
32
+ facets: {
33
+ type: Array,
34
+ default: () => []
35
+ }
36
+ });
37
+
38
+ const emit = defineEmits(['facet-change']);
39
+
40
+ function onFilterChange(group, event) {
41
+ const selectedUid = event.target.value;
42
+
43
+ const currentlySelected = group.children.find(c => c.selected);
44
+ if (currentlySelected?.uid === selectedUid) return;
45
+
46
+ group.children.forEach(facet => {
47
+ const shouldBeSelected = facet.uid === selectedUid;
48
+ if (facet.selected !== shouldBeSelected) {
49
+ emit('facet-change', {
50
+ groupUid: group.uid,
51
+ facetUid: facet.uid,
52
+ selected: shouldBeSelected
53
+ });
54
+ }
55
+ });
56
+ }
57
+ </script>
58
+
59
+ <style lang="scss">
60
+ .UluFacetsDropdownFilters {
61
+ display: flex;
62
+ gap: 1rem;
63
+ align-items: center;
64
+ flex-wrap: wrap;
65
+ }
66
+ .UluFacetsDropdownFilters__group {
67
+ display: flex;
68
+ gap: 0.5rem;
69
+ align-items: center;
70
+ }
71
+ </style>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <div class="UluFacetsHeaderLayout">
3
+ <div class="UluFacetsHeaderLayout__header">
4
+ <slot name="header"></slot>
5
+ </div>
6
+ <div class="UluFacetsHeaderLayout__main">
7
+ <slot name="main"></slot>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ // This component is purely for layout, no logic needed.
14
+ </script>
15
+
16
+ <style lang="scss">
17
+ .UluFacetsHeaderLayout__header {
18
+ display: flex;
19
+ gap: 1rem;
20
+ align-items: center;
21
+ margin-bottom: 1.5rem;
22
+ flex-wrap: wrap;
23
+ }
24
+ </style>
@@ -1,38 +1,67 @@
1
1
  <template>
2
- <ul class="UluFacets__facet-list">
3
- <li
4
- class="UluFacets__facet"
5
- :class="classFacet"
6
- v-for="facet in children"
7
- :key="facet.uid"
8
- >
9
- <input
10
- class="UluFacets__facet-checkbox"
11
- :id="facetCheckboxId(facet)"
12
- type="checkbox"
13
- :checked="facet.selected"
14
- @change="emit('facet-change', { groupUid, facetUid: facet.uid, selected: $event.target.checked })"
15
- >
16
- <label
17
- class="UluFacets__facet-label"
18
- :for="facetCheckboxId(facet)"
19
- >
20
- {{ facet.label }}
21
- </label>
22
- </li>
23
- </ul>
2
+ <UluSelectableMenu
3
+ class="UluFacets__facet-list"
4
+ :legend="groupUid"
5
+ :type="type"
6
+ :options="menuOptions"
7
+ :model-value="modelValue"
8
+ @update:model-value="handleChange"
9
+ >
10
+ <template #default="{ option }">
11
+ {{ option.label }}
12
+ </template>
13
+ </UluSelectableMenu>
24
14
  </template>
25
15
 
26
16
  <script setup>
27
- const props = defineProps({
28
- groupUid: String,
29
- children: Array,
30
- classFacet: String
31
- });
17
+ import { computed } from 'vue';
18
+ import UluSelectableMenu from '../../forms/UluSelectableMenu.vue';
32
19
 
33
- const emit = defineEmits(['facet-change']);
20
+ const props = defineProps({
21
+ groupUid: String,
22
+ groupName: String,
23
+ children: Array,
24
+ type: {
25
+ type: String,
26
+ default: 'checkbox',
27
+ },
28
+ modelValue: [String, Array],
29
+ });
34
30
 
35
- function facetCheckboxId(facet) {
36
- return `facet-${props.groupUid}-${facet.uid}`;
31
+ const emit = defineEmits(['facet-change']);
32
+
33
+ const menuOptions = computed(() => {
34
+ if (props.type === 'radio') {
35
+ return [{ label: `All ${props.groupName}s`, uid: '' }, ...props.children];
36
+ }
37
+ return props.children;
38
+ });
39
+
40
+ function handleChange(value) {
41
+ if (props.type === 'radio') {
42
+ const selectedUid = value;
43
+ props.children.forEach(facet => {
44
+ const shouldBeSelected = facet.uid === selectedUid;
45
+ if (facet.selected !== shouldBeSelected) {
46
+ emit('facet-change', {
47
+ groupUid: props.groupUid,
48
+ facetUid: facet.uid,
49
+ selected: shouldBeSelected
50
+ });
51
+ }
52
+ });
53
+ } else {
54
+ const selectedUids = new Set(value);
55
+ props.children.forEach(facet => {
56
+ const shouldBeSelected = selectedUids.has(facet.uid);
57
+ if (facet.selected !== shouldBeSelected) {
58
+ emit('facet-change', {
59
+ groupUid: props.groupUid,
60
+ facetUid: facet.uid,
61
+ selected: shouldBeSelected
62
+ });
63
+ }
64
+ });
37
65
  }
66
+ }
38
67
  </script>
@@ -1,13 +1,15 @@
1
1
  <template>
2
2
  <div class="UluFacetsResults">
3
- <transition-group
3
+ <transition-group
4
4
  v-if="items.length"
5
- :tag="tag"
5
+ :tag="tag"
6
6
  :name="transitionName"
7
7
  class="UluFacetsResults__list"
8
+ :class="classes.list"
8
9
  >
9
10
  <li
10
11
  class="UluFacetsResults__item"
12
+ :class="classes.item"
11
13
  v-for="(item, index) in items"
12
14
  :key="item.id || index"
13
15
  >
@@ -35,6 +37,10 @@
35
37
  transitionName: {
36
38
  type: String,
37
39
  default: 'UluFacetsFade'
40
+ },
41
+ classes: {
42
+ type: Object,
43
+ default: () => ({})
38
44
  }
39
45
  });
40
46
  </script>
@@ -1,5 +1,8 @@
1
1
  export { useFacets } from './facets/useFacets.js';
2
- export { default as UluFacetsFilters } from './facets/UluFacetsFilters.vue';
2
+ export { default as UluFacetsFilterLists } from './facets/UluFacetsFilterLists.vue';
3
+ export { default as UluFacetsFilterPopovers } from './facets/UluFacetsFilterPopovers.vue';
4
+ export { default as UluFacetsFilterSelects } from './facets/UluFacetsFilterSelects.vue';
5
+ export { default as UluFacetsHeaderLayout } from './facets/UluFacetsHeaderLayout.vue';
3
6
  export { default as UluFacetsResults } from './facets/UluFacetsResults.vue';
4
7
  export { default as UluFacetsSearch } from './facets/UluFacetsSearch.vue';
5
8
  export { default as UluFacetsSidebarLayout } from './facets/UluFacetsSidebarLayout.vue';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.1.0-beta.26",
3
+ "version": "0.1.0-beta.28",
4
4
  "description": "A modular and tree-shakeable Vue 3 component library for the Ulu frontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -38,7 +38,7 @@
38
38
  "build": "vite build",
39
39
  "types": "npx tsc",
40
40
  "deploy": "npm run types && npm run build && npm run docs:build",
41
- "update-contexts": "rm -rf .contexts && mkdir -p .contexts/frontend && cp -R node_modules/@ulu/frontend/scss node_modules/@ulu/frontend/js .contexts/frontend/"
41
+ "update-contexts": "rm -rf .ctx && mkdir -p .ctx/frontend && cp -R node_modules/@ulu/frontend/scss node_modules/@ulu/frontend/js .ctx/frontend/"
42
42
  },
43
43
  "keywords": [
44
44
  "vue",
@@ -59,7 +59,7 @@
59
59
  "homepage": "https://github.com/Jscherbe/frontend-vue#readme",
60
60
  "peerDependencies": {
61
61
  "@headlessui/vue": "^1.7.23",
62
- "@ulu/frontend": "^0.1.0-beta.102",
62
+ "@ulu/frontend": "^0.1.0-beta.104",
63
63
  "@unhead/vue": "^2.0.11",
64
64
  "vue": "^3.5.17",
65
65
  "vue-router": "^4.5.1"
@@ -82,7 +82,7 @@
82
82
  "@storybook/addon-essentials": "^9.0.0-alpha.12",
83
83
  "@storybook/addon-links": "^9.1.1",
84
84
  "@storybook/vue3-vite": "^9.1.1",
85
- "@ulu/frontend": "^0.1.0-beta.102",
85
+ "@ulu/frontend": "^0.1.0-beta.104",
86
86
  "@unhead/vue": "^2.0.11",
87
87
  "@vitejs/plugin-vue": "^6.0.0",
88
88
  "ollama": "^0.5.16",
@@ -1,37 +0,0 @@
1
- <template>
2
- <div class="menu-stack form-theme">
3
- <ul class="menu-stack__list">
4
- <li
5
- class="menu-stack__item"
6
- v-for="(option, index) in options"
7
- :key="index"
8
- >
9
- <div class="menu-stack__selectable">
10
- <input
11
- type="checkbox"
12
- :id="getId(index)"
13
- v-model="option.checked"
14
- >
15
- <label :for="getId(index)">
16
- <slot>
17
- {{ option?.title || option?.text }}
18
- </slot>
19
- </label>
20
- </div>
21
- </li>
22
- </ul>
23
- </div>
24
- </template>
25
-
26
- <script setup>
27
- defineProps({
28
- /**
29
- * Checkbox items in [{ title|text, checked }, ...]
30
- */
31
- options: Array
32
- });
33
-
34
- const getId = (index) => {
35
- return `checkbox-menu-opt-${ index }`;
36
- };
37
- </script>