@thxgg/steward 0.1.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 (154) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +175 -0
  4. package/app/app.vue +14 -0
  5. package/app/assets/css/main.css +129 -0
  6. package/app/components/CommandPalette.vue +182 -0
  7. package/app/components/ShortcutsHelp.vue +85 -0
  8. package/app/components/git/ChangesMinimap.vue +143 -0
  9. package/app/components/git/CommitList.vue +224 -0
  10. package/app/components/git/DiffPanel.vue +402 -0
  11. package/app/components/git/DiffViewer.vue +803 -0
  12. package/app/components/layout/RepoSelector.vue +358 -0
  13. package/app/components/layout/Sidebar.vue +91 -0
  14. package/app/components/prd/Meta.vue +69 -0
  15. package/app/components/prd/Viewer.vue +285 -0
  16. package/app/components/tasks/Board.vue +86 -0
  17. package/app/components/tasks/Card.vue +108 -0
  18. package/app/components/tasks/Column.vue +108 -0
  19. package/app/components/tasks/Detail.vue +291 -0
  20. package/app/components/ui/badge/Badge.vue +26 -0
  21. package/app/components/ui/badge/index.ts +26 -0
  22. package/app/components/ui/button/Button.vue +29 -0
  23. package/app/components/ui/button/index.ts +38 -0
  24. package/app/components/ui/card/Card.vue +22 -0
  25. package/app/components/ui/card/CardAction.vue +17 -0
  26. package/app/components/ui/card/CardContent.vue +17 -0
  27. package/app/components/ui/card/CardDescription.vue +17 -0
  28. package/app/components/ui/card/CardFooter.vue +17 -0
  29. package/app/components/ui/card/CardHeader.vue +17 -0
  30. package/app/components/ui/card/CardTitle.vue +17 -0
  31. package/app/components/ui/card/index.ts +7 -0
  32. package/app/components/ui/combobox/Combobox.vue +19 -0
  33. package/app/components/ui/combobox/ComboboxAnchor.vue +23 -0
  34. package/app/components/ui/combobox/ComboboxEmpty.vue +21 -0
  35. package/app/components/ui/combobox/ComboboxGroup.vue +27 -0
  36. package/app/components/ui/combobox/ComboboxInput.vue +42 -0
  37. package/app/components/ui/combobox/ComboboxItem.vue +24 -0
  38. package/app/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
  39. package/app/components/ui/combobox/ComboboxList.vue +33 -0
  40. package/app/components/ui/combobox/ComboboxSeparator.vue +21 -0
  41. package/app/components/ui/combobox/ComboboxTrigger.vue +24 -0
  42. package/app/components/ui/combobox/ComboboxViewport.vue +23 -0
  43. package/app/components/ui/combobox/index.ts +13 -0
  44. package/app/components/ui/command/Command.vue +103 -0
  45. package/app/components/ui/command/CommandDialog.vue +33 -0
  46. package/app/components/ui/command/CommandEmpty.vue +27 -0
  47. package/app/components/ui/command/CommandGroup.vue +45 -0
  48. package/app/components/ui/command/CommandInput.vue +54 -0
  49. package/app/components/ui/command/CommandItem.vue +76 -0
  50. package/app/components/ui/command/CommandList.vue +25 -0
  51. package/app/components/ui/command/CommandSeparator.vue +21 -0
  52. package/app/components/ui/command/CommandShortcut.vue +17 -0
  53. package/app/components/ui/command/index.ts +25 -0
  54. package/app/components/ui/dialog/Dialog.vue +19 -0
  55. package/app/components/ui/dialog/DialogClose.vue +15 -0
  56. package/app/components/ui/dialog/DialogContent.vue +53 -0
  57. package/app/components/ui/dialog/DialogDescription.vue +23 -0
  58. package/app/components/ui/dialog/DialogFooter.vue +15 -0
  59. package/app/components/ui/dialog/DialogHeader.vue +17 -0
  60. package/app/components/ui/dialog/DialogOverlay.vue +21 -0
  61. package/app/components/ui/dialog/DialogScrollContent.vue +59 -0
  62. package/app/components/ui/dialog/DialogTitle.vue +23 -0
  63. package/app/components/ui/dialog/DialogTrigger.vue +15 -0
  64. package/app/components/ui/dialog/index.ts +10 -0
  65. package/app/components/ui/input/Input.vue +33 -0
  66. package/app/components/ui/input/index.ts +1 -0
  67. package/app/components/ui/scroll-area/ScrollArea.vue +33 -0
  68. package/app/components/ui/scroll-area/ScrollBar.vue +32 -0
  69. package/app/components/ui/scroll-area/index.ts +2 -0
  70. package/app/components/ui/separator/Separator.vue +29 -0
  71. package/app/components/ui/separator/index.ts +1 -0
  72. package/app/components/ui/sheet/Sheet.vue +19 -0
  73. package/app/components/ui/sheet/SheetClose.vue +15 -0
  74. package/app/components/ui/sheet/SheetContent.vue +62 -0
  75. package/app/components/ui/sheet/SheetDescription.vue +21 -0
  76. package/app/components/ui/sheet/SheetFooter.vue +16 -0
  77. package/app/components/ui/sheet/SheetHeader.vue +15 -0
  78. package/app/components/ui/sheet/SheetOverlay.vue +21 -0
  79. package/app/components/ui/sheet/SheetTitle.vue +21 -0
  80. package/app/components/ui/sheet/SheetTrigger.vue +15 -0
  81. package/app/components/ui/sheet/index.ts +8 -0
  82. package/app/components/ui/tabs/Tabs.vue +24 -0
  83. package/app/components/ui/tabs/TabsContent.vue +21 -0
  84. package/app/components/ui/tabs/TabsList.vue +24 -0
  85. package/app/components/ui/tabs/TabsTrigger.vue +26 -0
  86. package/app/components/ui/tabs/index.ts +4 -0
  87. package/app/components/ui/tooltip/Tooltip.vue +19 -0
  88. package/app/components/ui/tooltip/TooltipContent.vue +34 -0
  89. package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
  90. package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
  91. package/app/components/ui/tooltip/index.ts +4 -0
  92. package/app/composables/useFileWatch.ts +78 -0
  93. package/app/composables/useGit.ts +180 -0
  94. package/app/composables/useKeyboard.ts +180 -0
  95. package/app/composables/usePrd.ts +86 -0
  96. package/app/composables/useRepos.ts +108 -0
  97. package/app/composables/useThemeMode.ts +38 -0
  98. package/app/composables/useToast.ts +31 -0
  99. package/app/layouts/default.vue +197 -0
  100. package/app/lib/utils.ts +7 -0
  101. package/app/pages/[repo]/[prd].vue +263 -0
  102. package/app/pages/index.vue +257 -0
  103. package/app/types/git.ts +81 -0
  104. package/app/types/index.ts +29 -0
  105. package/app/types/prd.ts +49 -0
  106. package/app/types/repo.ts +37 -0
  107. package/app/types/task.ts +134 -0
  108. package/bin/prd +21 -0
  109. package/components.json +21 -0
  110. package/dist/app/types/git.js +1 -0
  111. package/dist/app/types/prd.js +1 -0
  112. package/dist/app/types/repo.js +1 -0
  113. package/dist/app/types/task.js +1 -0
  114. package/dist/host/src/api/git.js +96 -0
  115. package/dist/host/src/api/index.js +4 -0
  116. package/dist/host/src/api/prds.js +195 -0
  117. package/dist/host/src/api/repos.js +47 -0
  118. package/dist/host/src/api/state.js +63 -0
  119. package/dist/host/src/executor.js +109 -0
  120. package/dist/host/src/index.js +95 -0
  121. package/dist/host/src/mcp.js +62 -0
  122. package/dist/host/src/ui.js +64 -0
  123. package/dist/server/utils/db.js +125 -0
  124. package/dist/server/utils/git.js +396 -0
  125. package/dist/server/utils/prd-state.js +229 -0
  126. package/dist/server/utils/repos.js +256 -0
  127. package/docs/MCP.md +180 -0
  128. package/nuxt.config.ts +34 -0
  129. package/package.json +88 -0
  130. package/public/favicon.ico +0 -0
  131. package/public/robots.txt +1 -0
  132. package/server/api/browse.get.ts +52 -0
  133. package/server/api/repos/[repoId]/git/commits.get.ts +103 -0
  134. package/server/api/repos/[repoId]/git/diff.get.ts +77 -0
  135. package/server/api/repos/[repoId]/git/file-content.get.ts +66 -0
  136. package/server/api/repos/[repoId]/git/file-diff.get.ts +109 -0
  137. package/server/api/repos/[repoId]/prd/[prdSlug]/progress.get.ts +36 -0
  138. package/server/api/repos/[repoId]/prd/[prdSlug]/tasks/[taskId]/commits.get.ts +146 -0
  139. package/server/api/repos/[repoId]/prd/[prdSlug]/tasks.get.ts +36 -0
  140. package/server/api/repos/[repoId]/prd/[prdSlug].get.ts +97 -0
  141. package/server/api/repos/[repoId]/prds.get.ts +85 -0
  142. package/server/api/repos/[repoId]/refresh-git-repos.post.ts +42 -0
  143. package/server/api/repos/[repoId].delete.ts +27 -0
  144. package/server/api/repos/index.get.ts +5 -0
  145. package/server/api/repos/index.post.ts +39 -0
  146. package/server/api/watch.get.ts +63 -0
  147. package/server/plugins/migrate-legacy-state.ts +19 -0
  148. package/server/tsconfig.json +3 -0
  149. package/server/utils/db.ts +169 -0
  150. package/server/utils/git.ts +478 -0
  151. package/server/utils/prd-state.ts +335 -0
  152. package/server/utils/repos.ts +322 -0
  153. package/server/utils/watcher.ts +179 -0
  154. package/tsconfig.json +4 -0
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ import { FilePlus, FileEdit, FileX, ArrowRight } from 'lucide-vue-next'
3
+ import type { FileDiff, FileStatus } from '~/types/git'
4
+
5
+ const props = defineProps<{
6
+ /** Array of file diffs to display */
7
+ files: FileDiff[]
8
+ /** Currently selected file path */
9
+ selectedFile?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ select: [path: string]
14
+ }>()
15
+
16
+ // Calculate max changes for proportional bar sizing
17
+ const maxChanges = computed(() => {
18
+ if (props.files.length === 0) return 1
19
+ return Math.max(...props.files.map(f => f.additions + f.deletions), 1)
20
+ })
21
+
22
+ // Get status icon and color
23
+ function getStatusConfig(status: FileStatus) {
24
+ switch (status) {
25
+ case 'added':
26
+ return { icon: FilePlus, class: 'text-green-600 dark:text-green-400' }
27
+ case 'deleted':
28
+ return { icon: FileX, class: 'text-red-600 dark:text-red-400' }
29
+ case 'renamed':
30
+ return { icon: ArrowRight, class: 'text-blue-600 dark:text-blue-400' }
31
+ case 'modified':
32
+ default:
33
+ return { icon: FileEdit, class: 'text-yellow-600 dark:text-yellow-400' }
34
+ }
35
+ }
36
+
37
+ // Calculate bar width percentages
38
+ function getBarWidths(file: FileDiff) {
39
+ const total = file.additions + file.deletions
40
+ if (total === 0) return { additions: 0, deletions: 0, total: 0 }
41
+
42
+ const totalWidth = (total / maxChanges.value) * 100
43
+ const additionsWidth = (file.additions / total) * totalWidth
44
+ const deletionsWidth = (file.deletions / total) * totalWidth
45
+
46
+ return {
47
+ additions: additionsWidth,
48
+ deletions: deletionsWidth,
49
+ total: totalWidth,
50
+ }
51
+ }
52
+
53
+ // Get display name for file (handle renames)
54
+ function getDisplayName(file: FileDiff): string {
55
+ if (file.status === 'renamed' && file.oldPath) {
56
+ return `${file.oldPath} → ${file.path}`
57
+ }
58
+ return file.path
59
+ }
60
+
61
+ // Get short name (just filename)
62
+ function getShortName(path: string): string {
63
+ return path.split('/').pop() || path
64
+ }
65
+
66
+ function handleClick(path: string) {
67
+ emit('select', path)
68
+ }
69
+ </script>
70
+
71
+ <template>
72
+ <div class="space-y-0.5 p-2">
73
+ <button
74
+ v-for="file in files"
75
+ :key="file.path"
76
+ class="group flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted/50"
77
+ :class="{
78
+ 'bg-muted': selectedFile === file.path,
79
+ }"
80
+ @click="handleClick(file.path)"
81
+ >
82
+ <!-- Status icon -->
83
+ <component
84
+ :is="getStatusConfig(file.status).icon"
85
+ class="size-4 shrink-0"
86
+ :class="getStatusConfig(file.status).class"
87
+ />
88
+
89
+ <!-- File info -->
90
+ <div class="min-w-0 flex-1">
91
+ <!-- File path -->
92
+ <div
93
+ class="truncate text-xs"
94
+ :class="{
95
+ 'font-medium': selectedFile === file.path,
96
+ }"
97
+ :title="getDisplayName(file)"
98
+ >
99
+ <template v-if="file.status === 'renamed' && file.oldPath">
100
+ <span class="text-muted-foreground">{{ getShortName(file.oldPath) }}</span>
101
+ <ArrowRight class="mx-1 inline size-3 text-muted-foreground" />
102
+ <span>{{ getShortName(file.path) }}</span>
103
+ </template>
104
+ <template v-else>
105
+ {{ file.path }}
106
+ </template>
107
+ </div>
108
+
109
+ <!-- Change bar -->
110
+ <div class="mt-1 flex h-1.5 w-full overflow-hidden rounded-full bg-muted">
111
+ <div
112
+ v-if="getBarWidths(file).additions > 0"
113
+ class="bg-green-500 dark:bg-green-400"
114
+ :style="{ width: `${getBarWidths(file).additions}%` }"
115
+ />
116
+ <div
117
+ v-if="getBarWidths(file).deletions > 0"
118
+ class="bg-red-500 dark:bg-red-400"
119
+ :style="{ width: `${getBarWidths(file).deletions}%` }"
120
+ />
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Stats -->
125
+ <div class="flex shrink-0 items-center gap-1 text-xs">
126
+ <span v-if="file.additions > 0" class="text-green-600 dark:text-green-400">
127
+ +{{ file.additions }}
128
+ </span>
129
+ <span v-if="file.deletions > 0" class="text-red-600 dark:text-red-400">
130
+ -{{ file.deletions }}
131
+ </span>
132
+ </div>
133
+ </button>
134
+
135
+ <!-- Empty state -->
136
+ <div
137
+ v-if="files.length === 0"
138
+ class="py-4 text-center text-sm text-muted-foreground"
139
+ >
140
+ No files changed
141
+ </div>
142
+ </div>
143
+ </template>
@@ -0,0 +1,224 @@
1
+ <script setup lang="ts">
2
+ import { GitCommit as GitCommitIcon, Plus, Minus, FileText, FolderGit2, AlertCircle } from 'lucide-vue-next'
3
+ import { Badge } from '~/components/ui/badge'
4
+ import type { GitCommit } from '~/types/git'
5
+ import type { CommitRef } from '~/types/task'
6
+
7
+ const props = defineProps<{
8
+ /** Array of commits to display - expects CommitRef objects with sha and repo */
9
+ commits: CommitRef[]
10
+ /** Repository ID for API calls */
11
+ repoId: string
12
+ }>()
13
+
14
+ const emit = defineEmits<{
15
+ select: [sha: string, repo?: string]
16
+ }>()
17
+
18
+ const { fetchCommits, isLoadingCommits } = useGit()
19
+
20
+ // Fetched commit details (keyed by sha for lookup)
21
+ const commitDetails = ref<Map<string, GitCommit>>(new Map())
22
+
23
+ // Track commits that failed to load
24
+ const failedCommits = ref<Set<string>>(new Set())
25
+
26
+ // Check if any commits have repo info (indicates pseudo-monorepo)
27
+ const hasMultipleRepos = computed(() => {
28
+ const repos = new Set(props.commits.map(c => c.repo).filter(Boolean))
29
+ return repos.size > 1
30
+ })
31
+
32
+ // Fetch commit details when props change
33
+ // Group commits by repo to make efficient API calls
34
+ watch(
35
+ () => ({ commits: props.commits, repoId: props.repoId }),
36
+ async ({ commits, repoId }) => {
37
+ if (commits.length === 0 || !repoId) {
38
+ commitDetails.value = new Map()
39
+ failedCommits.value = new Set()
40
+ return
41
+ }
42
+
43
+ // Group commits by repo
44
+ const commitsByRepo = new Map<string, string[]>()
45
+ for (const commit of commits) {
46
+ const repoPath = commit.repo || ''
47
+ if (!commitsByRepo.has(repoPath)) {
48
+ commitsByRepo.set(repoPath, [])
49
+ }
50
+ commitsByRepo.get(repoPath)!.push(commit.sha)
51
+ }
52
+
53
+ // Fetch commits for each repo in parallel
54
+ const results = await Promise.all(
55
+ Array.from(commitsByRepo.entries()).map(async ([repoPath, shas]) => {
56
+ const result = await fetchCommits(repoId, shas, repoPath || undefined)
57
+ return {
58
+ commits: result.commits.map(c => ({ ...c, repoPath })),
59
+ failedShas: result.failedShas,
60
+ }
61
+ })
62
+ )
63
+
64
+ // Build map of sha -> commit details and collect failed SHAs
65
+ const detailsMap = new Map<string, GitCommit>()
66
+ const failed = new Set<string>()
67
+
68
+ for (const { commits: repoCommits, failedShas } of results) {
69
+ for (const commit of repoCommits) {
70
+ // Key by shortSha since props contain abbreviated SHAs
71
+ detailsMap.set(commit.shortSha, commit)
72
+ }
73
+ for (const sha of failedShas) {
74
+ failed.add(sha)
75
+ }
76
+ }
77
+
78
+ commitDetails.value = detailsMap
79
+ failedCommits.value = failed
80
+ },
81
+ { immediate: true }
82
+ )
83
+
84
+ // Format relative date
85
+ function formatRelativeDate(isoDate: string): string {
86
+ const date = new Date(isoDate)
87
+ const now = new Date()
88
+ const diffMs = now.getTime() - date.getTime()
89
+ const diffSeconds = Math.floor(diffMs / 1000)
90
+ const diffMinutes = Math.floor(diffSeconds / 60)
91
+ const diffHours = Math.floor(diffMinutes / 60)
92
+ const diffDays = Math.floor(diffHours / 24)
93
+
94
+ if (diffSeconds < 60) {
95
+ return 'just now'
96
+ } else if (diffMinutes < 60) {
97
+ return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`
98
+ } else if (diffHours < 24) {
99
+ return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
100
+ } else if (diffDays < 7) {
101
+ return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`
102
+ } else {
103
+ return date.toLocaleDateString()
104
+ }
105
+ }
106
+
107
+ function handleClick(commit: CommitRef) {
108
+ emit('select', commit.sha, commit.repo)
109
+ }
110
+
111
+ // Get commit details by SHA (supports prefix matching for abbreviated SHAs)
112
+ function getDetails(sha: string): GitCommit | undefined {
113
+ // Direct lookup first
114
+ const direct = commitDetails.value.get(sha)
115
+ if (direct) return direct
116
+
117
+ // Try prefix matching (short SHA might be abbreviated differently)
118
+ for (const [key, commit] of commitDetails.value) {
119
+ if (key.startsWith(sha) || sha.startsWith(key)) {
120
+ return commit
121
+ }
122
+ }
123
+ return undefined
124
+ }
125
+
126
+ // Check if a commit failed to load (supports prefix matching)
127
+ function isFailed(sha: string): boolean {
128
+ if (failedCommits.value.has(sha)) return true
129
+
130
+ // Try prefix matching
131
+ for (const failedSha of failedCommits.value) {
132
+ if (failedSha.startsWith(sha) || sha.startsWith(failedSha)) {
133
+ return true
134
+ }
135
+ }
136
+ return false
137
+ }
138
+ </script>
139
+
140
+ <template>
141
+ <div class="space-y-2">
142
+ <!-- Loading skeleton -->
143
+ <template v-if="isLoadingCommits">
144
+ <div
145
+ v-for="i in Math.min(commits.length, 3)"
146
+ :key="i"
147
+ class="animate-pulse rounded-lg border bg-muted/50 p-3"
148
+ >
149
+ <div class="flex items-start gap-3">
150
+ <div class="h-5 w-5 rounded bg-muted" />
151
+ <div class="flex-1 space-y-2">
152
+ <div class="h-4 w-20 rounded bg-muted" />
153
+ <div class="h-4 w-3/4 rounded bg-muted" />
154
+ <div class="h-3 w-1/2 rounded bg-muted" />
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </template>
159
+
160
+ <!-- Empty state -->
161
+ <div
162
+ v-else-if="commits.length === 0"
163
+ class="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground"
164
+ >
165
+ No commits recorded for this task
166
+ </div>
167
+
168
+ <!-- Commit list -->
169
+ <template v-else>
170
+ <button
171
+ v-for="commit in commits"
172
+ :key="commit.sha"
173
+ class="w-full rounded-lg border bg-card p-3 text-left transition-colors hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring"
174
+ @click="handleClick(commit)"
175
+ >
176
+ <div class="flex items-start gap-3">
177
+ <GitCommitIcon class="mt-0.5 size-5 shrink-0 text-muted-foreground" />
178
+ <div class="min-w-0 flex-1">
179
+ <!-- SHA, stats, and repo badge -->
180
+ <div class="flex flex-wrap items-center gap-2">
181
+ <code class="font-mono text-xs font-medium text-primary">
182
+ {{ getDetails(commit.sha)?.shortSha || commit.sha.substring(0, 7) }}
183
+ </code>
184
+ <template v-if="getDetails(commit.sha)">
185
+ <span class="flex items-center gap-1 text-xs text-muted-foreground">
186
+ <FileText class="size-3" />
187
+ {{ getDetails(commit.sha)!.filesChanged }}
188
+ </span>
189
+ <span class="flex items-center gap-0.5 text-xs text-green-600 dark:text-green-400">
190
+ <Plus class="size-3" />{{ getDetails(commit.sha)!.additions }}
191
+ </span>
192
+ <span class="flex items-center gap-0.5 text-xs text-red-600 dark:text-red-400">
193
+ <Minus class="size-3" />{{ getDetails(commit.sha)!.deletions }}
194
+ </span>
195
+ </template>
196
+ <!-- Repo badge - only show for pseudo-monorepos with multiple repos -->
197
+ <Badge v-if="commit.repo && hasMultipleRepos" variant="secondary" class="gap-1 text-xs">
198
+ <FolderGit2 class="size-3" />
199
+ {{ commit.repo }}
200
+ </Badge>
201
+ </div>
202
+
203
+ <!-- Message -->
204
+ <p class="mt-1 truncate text-sm">
205
+ <template v-if="getDetails(commit.sha)">
206
+ {{ getDetails(commit.sha)!.message }}
207
+ </template>
208
+ <span v-else-if="isFailed(commit.sha)" class="flex items-center gap-1 text-muted-foreground">
209
+ <AlertCircle class="size-3" />
210
+ Commit unavailable
211
+ </span>
212
+ <span v-else class="text-muted-foreground">Loading...</span>
213
+ </p>
214
+
215
+ <!-- Author and date -->
216
+ <p v-if="getDetails(commit.sha)" class="mt-1 text-xs text-muted-foreground">
217
+ {{ getDetails(commit.sha)!.author }} &middot; {{ formatRelativeDate(getDetails(commit.sha)!.date) }}
218
+ </p>
219
+ </div>
220
+ </div>
221
+ </button>
222
+ </template>
223
+ </div>
224
+ </template>