@hina-ui/vue 1.5.0 → 1.6.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 (68) hide show
  1. package/dist/index.js +831 -390
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/app-shell/AppShell.vue.d.ts +1 -0
  4. package/dist/types/components/app-shell/AppShell.vue.d.ts.map +1 -1
  5. package/dist/types/components/combobox/Combobox.vue.d.ts +2 -1
  6. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  7. package/dist/types/components/combobox/composables/useCombobox.d.ts +25 -0
  8. package/dist/types/components/combobox/composables/useCombobox.d.ts.map +1 -0
  9. package/dist/types/components/combobox/composables/useOptionLabels.d.ts +3 -0
  10. package/dist/types/components/combobox/composables/useOptionLabels.d.ts.map +1 -0
  11. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +1 -0
  12. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
  13. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +2 -1
  14. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -1
  15. package/dist/types/components/nav-link/NavLink.vue.d.ts.map +1 -1
  16. package/dist/types/components/nav-link/nav-link.variants.d.ts +1 -18
  17. package/dist/types/components/nav-link/nav-link.variants.d.ts.map +1 -1
  18. package/dist/types/components/sidebar/Sidebar.vue.d.ts +15 -11
  19. package/dist/types/components/sidebar/Sidebar.vue.d.ts.map +1 -1
  20. package/dist/types/components/sidebar/SidebarGroup.vue.d.ts.map +1 -1
  21. package/dist/types/components/sidebar/SidebarLabel.vue.d.ts +20 -0
  22. package/dist/types/components/sidebar/SidebarLabel.vue.d.ts.map +1 -0
  23. package/dist/types/components/sidebar/sidebar.variants.d.ts +46 -0
  24. package/dist/types/components/sidebar/sidebar.variants.d.ts.map +1 -0
  25. package/dist/types/components/tree/Tree.vue.d.ts +37 -0
  26. package/dist/types/components/tree/Tree.vue.d.ts.map +1 -0
  27. package/dist/types/components/tree/TreeCheck.vue.d.ts +8 -0
  28. package/dist/types/components/tree/TreeCheck.vue.d.ts.map +1 -0
  29. package/dist/types/components/tree/composables/useTree.d.ts +23 -0
  30. package/dist/types/components/tree/composables/useTree.d.ts.map +1 -0
  31. package/dist/types/components/tree/tree.variants.d.ts +5 -0
  32. package/dist/types/components/tree/tree.variants.d.ts.map +1 -0
  33. package/dist/types/components/tree/types.d.ts +18 -0
  34. package/dist/types/components/tree/types.d.ts.map +1 -0
  35. package/dist/types/components/tree/utils/selection.d.ts +16 -0
  36. package/dist/types/components/tree/utils/selection.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +3 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/locale/en-US.d.ts.map +1 -1
  40. package/dist/types/locale/index.d.ts.map +1 -1
  41. package/dist/types/locale/types.d.ts +3 -0
  42. package/dist/types/locale/types.d.ts.map +1 -1
  43. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/app-shell/AppShell.vue +2 -1
  46. package/src/components/combobox/Combobox.vue +14 -23
  47. package/src/components/combobox/composables/useCombobox.ts +63 -0
  48. package/src/components/combobox/composables/useOptionLabels.ts +20 -0
  49. package/src/components/multi-combobox/MultiCombobox.vue +2 -0
  50. package/src/components/multi-combobox/composables/useMultiCombobox.ts +5 -10
  51. package/src/components/nav-link/NavLink.vue +7 -1
  52. package/src/components/nav-link/nav-link.variants.ts +1 -10
  53. package/src/components/sidebar/Sidebar.vue +43 -33
  54. package/src/components/sidebar/SidebarGroup.vue +12 -11
  55. package/src/components/sidebar/SidebarLabel.vue +24 -0
  56. package/src/components/sidebar/sidebar.variants.ts +36 -0
  57. package/src/components/tree/Tree.vue +132 -0
  58. package/src/components/tree/TreeCheck.vue +33 -0
  59. package/src/components/tree/composables/useTree.ts +81 -0
  60. package/src/components/tree/tree.variants.ts +22 -0
  61. package/src/components/tree/types.ts +20 -0
  62. package/src/components/tree/utils/selection.ts +78 -0
  63. package/src/index.ts +3 -0
  64. package/src/locale/en-US.ts +1 -0
  65. package/src/locale/index.ts +1 -0
  66. package/src/locale/types.ts +3 -0
  67. package/src/locale/zh-CN.ts +1 -0
  68. package/src/styles/motion.css +40 -0
@@ -0,0 +1,81 @@
1
+ import { computed, type Ref } from 'vue'
2
+ import type { TreeNode, TreeNodeState, TreeValue } from '../types'
3
+ import { checkedState, indexTree, toggleChecked } from '../utils/selection'
4
+
5
+ interface Options {
6
+ items: TreeNode[]
7
+ multiple?: boolean
8
+ defaultExpanded: TreeValue[]
9
+ }
10
+
11
+ export function useTree(
12
+ props: Options,
13
+ model: Ref<TreeValue | TreeValue[] | null | undefined>,
14
+ expandedModel: Ref<TreeValue[] | undefined>,
15
+ disabled: Ref<boolean>,
16
+ ) {
17
+ const initialExpanded = [...props.defaultExpanded]
18
+ const index = computed(() => indexTree(props.items))
19
+ const states = computed(() => {
20
+ if (props.multiple)
21
+ return checkedState(index.value, Array.isArray(model.value) ? model.value : [])
22
+ return new Map(
23
+ [...index.value.entries].map(([value, entry]) => [
24
+ value,
25
+ {
26
+ selected: value === model.value,
27
+ indeterminate: false,
28
+ disabled: entry.disabled,
29
+ },
30
+ ]),
31
+ )
32
+ })
33
+ const selected = computed(() =>
34
+ [...index.value.entries.values()]
35
+ .filter(entry => states.value.get(entry.node.value)?.selected)
36
+ .map(entry => entry.node),
37
+ )
38
+ const keys = computed(
39
+ () => new Map([...index.value.entries].map(([value, entry]) => [key(entry.node), value])),
40
+ )
41
+ const expanded = computed({
42
+ get: () => (expandedModel.value ?? initialExpanded).map(value => key({ value })),
43
+ set: value => {
44
+ expandedModel.value = value.flatMap(key =>
45
+ keys.value.has(key) ? [keys.value.get(key)!] : [],
46
+ )
47
+ },
48
+ })
49
+
50
+ function key(node: Pick<TreeNode, 'value'>) {
51
+ return `${typeof node.value}:${node.value}`
52
+ }
53
+ function getChildren(node: TreeNode) {
54
+ return node.children?.length ? node.children : undefined
55
+ }
56
+ function state(node: TreeNode): TreeNodeState {
57
+ const value = states.value.get(node.value)!
58
+ return { ...value, disabled: disabled.value || value.disabled }
59
+ }
60
+ function select(event: CustomEvent<{ value?: TreeNode }>) {
61
+ event.preventDefault()
62
+ const node = event.detail.value
63
+ if (!node || state(node).disabled) return
64
+ model.value = props.multiple
65
+ ? toggleChecked(index.value, Array.isArray(model.value) ? model.value : [], node.value)
66
+ : model.value === node.value
67
+ ? null
68
+ : node.value
69
+ }
70
+ function toggle(node: TreeNode) {
71
+ if (state(node).disabled || !getChildren(node)) return
72
+ const value = key(node)
73
+ expanded.value = expanded.value.includes(value)
74
+ ? expanded.value.filter(key => key !== value)
75
+ : [...expanded.value, value]
76
+ }
77
+ function keepRowClick(event: CustomEvent<{ originalEvent: Event }>) {
78
+ if (event.detail.originalEvent.type === 'click') event.preventDefault()
79
+ }
80
+ return { selected, expanded, key, getChildren, state, select, toggle, keepRowClick }
81
+ }
@@ -0,0 +1,22 @@
1
+ import { tv } from '../../lib/tv'
2
+
3
+ export const tree = tv({
4
+ base: 'flex w-full min-w-0 flex-col outline-none',
5
+ })
6
+
7
+ export const treeRow = tv({
8
+ base: [
9
+ 'hn-interactive hn-state-layer hn-press-none relative isolate flex w-full cursor-pointer',
10
+ 'items-center gap-(--hn-control-gap) rounded-md py-2 pe-2.5 text-start text-sm select-none',
11
+ 'ps-[calc(0.375rem+var(--hn-tree-level)*1.25rem)] text-fg outline-none',
12
+ 'data-disabled:pointer-events-none data-disabled:opacity-50',
13
+ ],
14
+ })
15
+
16
+ export const treeToggle = tv({
17
+ base: 'text-muted flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm rtl:-scale-x-100 [&>span>svg]:size-3.5',
18
+ })
19
+
20
+ export const treeEmpty = tv({
21
+ base: 'text-muted px-2.5 py-2 text-sm',
22
+ })
@@ -0,0 +1,20 @@
1
+ export type TreeValue = string | number
2
+
3
+ export interface TreeNode {
4
+ value: TreeValue
5
+ label: string
6
+ description?: string
7
+ disabled?: boolean
8
+ children?: TreeNode[]
9
+ }
10
+
11
+ export interface TreeNodeState {
12
+ selected: boolean
13
+ indeterminate: boolean
14
+ disabled: boolean
15
+ }
16
+
17
+ export interface TreeNodeSlot extends TreeNodeState {
18
+ node: TreeNode
19
+ expanded: boolean
20
+ }
@@ -0,0 +1,78 @@
1
+ import type { TreeNode, TreeNodeState, TreeValue } from '../types'
2
+
3
+ interface Entry {
4
+ node: TreeNode
5
+ parent?: TreeValue
6
+ children: TreeValue[]
7
+ disabled: boolean
8
+ }
9
+
10
+ export interface TreeIndex {
11
+ roots: TreeValue[]
12
+ entries: Map<TreeValue, Entry>
13
+ }
14
+
15
+ export function indexTree(nodes: TreeNode[]): TreeIndex {
16
+ const entries = new Map<TreeValue, Entry>()
17
+ function visit(node: TreeNode, parent?: TreeValue, disabled = false) {
18
+ const entry = {
19
+ node,
20
+ parent,
21
+ disabled: disabled || !!node.disabled,
22
+ children: node.children?.map(child => child.value) ?? [],
23
+ }
24
+ entries.set(node.value, entry)
25
+ node.children?.forEach(child => visit(child, node.value, entry.disabled))
26
+ }
27
+ nodes.forEach(node => visit(node))
28
+ return { roots: nodes.map(node => node.value), entries }
29
+ }
30
+
31
+ export function checkedState(tree: TreeIndex, values: TreeValue[]) {
32
+ const checked = new Set(values)
33
+ const states = new Map<TreeValue, TreeNodeState>()
34
+ function visit(value: TreeValue, inherited = false): TreeNodeState {
35
+ const entry = tree.entries.get(value)!
36
+ const own = checked.has(value) || (!entry.disabled && inherited)
37
+ const children = entry.children.map(child => visit(child, !entry.disabled && own))
38
+ const enabled = children.filter(child => !child.disabled)
39
+ const selected =
40
+ entry.disabled || enabled.length === 0 ? own : enabled.every(child => child.selected)
41
+ const indeterminate =
42
+ !entry.disabled && !selected && enabled.some(child => child.selected || child.indeterminate)
43
+ const state = { selected, indeterminate, disabled: entry.disabled }
44
+ states.set(value, state)
45
+ return state
46
+ }
47
+ tree.roots.forEach(value => visit(value))
48
+ return states
49
+ }
50
+
51
+ export function toggleChecked(tree: TreeIndex, values: TreeValue[], value: TreeValue): TreeValue[] {
52
+ const entry = tree.entries.get(value)
53
+ if (!entry || entry.disabled) return values
54
+ const states = checkedState(tree, values)
55
+ const checked = new Set(values.filter(value => !tree.entries.has(value)))
56
+ states.forEach((state, value) => {
57
+ if (state.selected) checked.add(value)
58
+ })
59
+ const selected = !states.get(value)!.selected
60
+ function visit(value: TreeValue) {
61
+ const entry = tree.entries.get(value)!
62
+ if (entry.disabled) return
63
+ if (selected) checked.add(value)
64
+ else checked.delete(value)
65
+ entry.children.forEach(visit)
66
+ }
67
+ visit(value)
68
+ let parent = entry.parent
69
+ while (parent !== undefined) {
70
+ checked.delete(parent)
71
+ parent = tree.entries.get(parent)!.parent
72
+ }
73
+ const next = checkedState(tree, [...checked])
74
+ return [
75
+ ...[...checked].filter(value => !tree.entries.has(value)),
76
+ ...[...tree.entries.keys()].filter(value => next.get(value)!.selected),
77
+ ]
78
+ }
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export { default as CommandPalette } from './components/command-palette/CommandP
62
62
  export type { CommandItem, CommandGroup, CommandItems } from './components/command-palette/types'
63
63
  export { default as Listbox } from './components/listbox/Listbox.vue'
64
64
  export type { ListboxVariants } from './components/listbox/listbox.variants'
65
+ export { default as Tree } from './components/tree/Tree.vue'
66
+ export type { TreeNode, TreeNodeSlot, TreeValue } from './components/tree/types'
65
67
  export { default as TreeSelect } from './components/tree-select/TreeSelect.vue'
66
68
  export type { TreeSelectNode } from './components/tree-select/types'
67
69
  export { default as Checkbox } from './components/checkbox/Checkbox.vue'
@@ -197,6 +199,7 @@ export { default as AccordionTrigger } from './components/accordion/AccordionTri
197
199
  export { default as AccordionContent } from './components/accordion/AccordionContent.vue'
198
200
  export { default as NavLink } from './components/nav-link/NavLink.vue'
199
201
  export { default as Sidebar } from './components/sidebar/Sidebar.vue'
202
+ export { default as SidebarLabel } from './components/sidebar/SidebarLabel.vue'
200
203
  export { default as SidebarGroup } from './components/sidebar/SidebarGroup.vue'
201
204
  export { default as AppShell } from './components/app-shell/AppShell.vue'
202
205
  export { default as Page } from './components/page/Page.vue'
@@ -105,6 +105,7 @@ export const enUS: UiMessages = {
105
105
  show: 'Show password',
106
106
  hide: 'Hide password',
107
107
  },
108
+ tree: { empty: 'No nodes' },
108
109
  treeSelect: {
109
110
  search: 'Search nodes',
110
111
  },
@@ -36,6 +36,7 @@ function merge(base: UiMessages, patch: PartialUiMessages): UiMessages {
36
36
  chip: { ...base.chip, ...patch.chip },
37
37
  numberInput: { ...base.numberInput, ...patch.numberInput },
38
38
  passwordInput: { ...base.passwordInput, ...patch.passwordInput },
39
+ tree: { ...base.tree, ...patch.tree },
39
40
  treeSelect: { ...base.treeSelect, ...patch.treeSelect },
40
41
  combobox: { ...base.combobox, ...patch.combobox },
41
42
  slider: { ...base.slider, ...patch.slider },
@@ -102,6 +102,9 @@ export interface UiMessages {
102
102
  show: string
103
103
  hide: string
104
104
  }
105
+ tree: {
106
+ empty: string
107
+ }
105
108
  treeSelect: {
106
109
  search: string
107
110
  }
@@ -105,6 +105,7 @@ export const zhCN: UiMessages = {
105
105
  show: '显示密码',
106
106
  hide: '隐藏密码',
107
107
  },
108
+ tree: { empty: '暂无节点' },
108
109
  treeSelect: {
109
110
  search: '搜索节点',
110
111
  },
@@ -616,3 +616,43 @@
616
616
  }
617
617
  }
618
618
  }
619
+
620
+ @utility hn-sidebar {
621
+ transition:
622
+ width var(--hn-duration-base) var(--hn-ease-move),
623
+ border-color var(--hn-duration-base) var(--hn-ease-move),
624
+ visibility 0s;
625
+
626
+ &[data-state='hidden'] {
627
+ visibility: hidden;
628
+ transition:
629
+ width var(--hn-duration-base) var(--hn-ease-move),
630
+ border-color var(--hn-duration-base) var(--hn-ease-move),
631
+ visibility 0s var(--hn-duration-base);
632
+ }
633
+
634
+ @media (prefers-reduced-motion: reduce) {
635
+ transition: none;
636
+ }
637
+ }
638
+
639
+ @utility hn-sidebar-label {
640
+ opacity: 1;
641
+ visibility: visible;
642
+ transition:
643
+ opacity var(--hn-duration-fast) var(--hn-ease-enter) var(--hn-duration-fast),
644
+ visibility 0s var(--hn-duration-fast);
645
+
646
+ &[data-collapsed] {
647
+ opacity: 0;
648
+ visibility: hidden;
649
+ pointer-events: none;
650
+ transition:
651
+ opacity calc(var(--hn-duration-fast) / 2) var(--hn-ease-exit),
652
+ visibility 0s calc(var(--hn-duration-fast) / 2);
653
+ }
654
+
655
+ @media (prefers-reduced-motion: reduce) {
656
+ transition: none;
657
+ }
658
+ }