@monkey-mini-app/ui 0.1.1 → 0.1.4

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 (203) hide show
  1. package/README.md +12 -6
  2. package/dist/globals.css +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/manifest.json +6 -6
  5. package/dist/runtime.js +9 -0
  6. package/dist/sdk.js +158 -0
  7. package/dist/src/blocks/activity-feed.tsx +7 -0
  8. package/dist/src/blocks/app-shell.tsx +1 -0
  9. package/dist/src/blocks/attachment-gallery.tsx +7 -0
  10. package/dist/src/blocks/bar-chart.tsx +11 -0
  11. package/dist/src/blocks/comment-thread.tsx +7 -0
  12. package/dist/src/blocks/dashboard-shell.tsx +109 -0
  13. package/dist/src/blocks/description-list.tsx +7 -0
  14. package/dist/src/blocks/detail-panel.tsx +1 -0
  15. package/dist/src/blocks/donut-chart.tsx +7 -0
  16. package/dist/src/blocks/env-badge.tsx +7 -0
  17. package/dist/src/blocks/env-table.tsx +316 -0
  18. package/dist/src/blocks/file-tree.tsx +7 -0
  19. package/dist/src/blocks/filter-bar.tsx +1 -0
  20. package/dist/src/blocks/form-sheet.tsx +160 -0
  21. package/dist/src/blocks/gauge.tsx +7 -0
  22. package/dist/src/blocks/list-detail.tsx +121 -0
  23. package/dist/src/blocks/notification-center.tsx +7 -0
  24. package/dist/src/blocks/page-header.tsx +1 -0
  25. package/dist/src/blocks/progress-ring.tsx +7 -0
  26. package/dist/src/blocks/radar-chart.tsx +7 -0
  27. package/dist/src/blocks/request-inspector.tsx +7 -0
  28. package/dist/src/blocks/reveal.tsx +56 -0
  29. package/dist/src/blocks/run-timeline.tsx +7 -0
  30. package/dist/src/blocks/scrollspy.tsx +7 -0
  31. package/dist/src/blocks/settings-split.tsx +154 -0
  32. package/dist/src/blocks/severity-chip.tsx +7 -0
  33. package/dist/src/blocks/sparkline.tsx +7 -0
  34. package/dist/src/blocks/stat-card.tsx +1 -0
  35. package/dist/src/blocks/status-badge.tsx +1 -0
  36. package/dist/src/blocks/table-page.tsx +83 -0
  37. package/dist/src/blocks/terminal.tsx +21 -3
  38. package/dist/src/blocks/test-step-list.tsx +7 -0
  39. package/dist/src/blocks/trend-card.tsx +7 -0
  40. package/dist/src/blocks/wizard-shell.tsx +132 -0
  41. package/dist/src/cdn-modules.d.ts +5 -0
  42. package/dist/src/components/accordion.tsx +7 -0
  43. package/dist/src/components/alert-dialog.tsx +7 -0
  44. package/dist/src/components/alert.tsx +7 -0
  45. package/dist/src/components/aspect-ratio.tsx +4 -0
  46. package/dist/src/components/attachment.tsx +4 -0
  47. package/dist/src/components/avatar.tsx +4 -0
  48. package/dist/src/components/badge.tsx +7 -0
  49. package/dist/src/components/breadcrumb.tsx +4 -0
  50. package/dist/src/components/bubble.tsx +4 -0
  51. package/dist/src/components/button-group.tsx +6 -0
  52. package/dist/src/components/button.tsx +7 -0
  53. package/dist/src/components/calendar.tsx +4 -0
  54. package/dist/src/components/card.tsx +8 -1
  55. package/dist/src/components/chart.tsx +7 -0
  56. package/dist/src/components/checkbox.tsx +5 -0
  57. package/dist/src/components/collapsible.tsx +4 -0
  58. package/dist/src/components/combobox.tsx +4 -0
  59. package/dist/src/components/command.tsx +6 -0
  60. package/dist/src/components/context-menu.tsx +4 -0
  61. package/dist/src/components/dialog.tsx +21 -0
  62. package/dist/src/components/drawer.tsx +4 -0
  63. package/dist/src/components/dropdown-menu.tsx +7 -0
  64. package/dist/src/components/empty.tsx +7 -0
  65. package/dist/src/components/field.tsx +6 -0
  66. package/dist/src/components/hover-card.tsx +4 -0
  67. package/dist/src/components/input-group.tsx +4 -0
  68. package/dist/src/components/input.tsx +7 -0
  69. package/dist/src/components/item.tsx +4 -0
  70. package/dist/src/components/kbd.tsx +4 -0
  71. package/dist/src/components/label.tsx +7 -0
  72. package/dist/src/components/marker.tsx +4 -0
  73. package/dist/src/components/menubar.tsx +4 -0
  74. package/dist/src/components/message-scroller.tsx +4 -0
  75. package/dist/src/components/message.tsx +4 -0
  76. package/dist/src/components/native-select.tsx +4 -0
  77. package/dist/src/components/navigation-menu.tsx +4 -0
  78. package/dist/src/components/pagination.tsx +4 -0
  79. package/dist/src/components/popover.tsx +7 -0
  80. package/dist/src/components/progress.tsx +7 -0
  81. package/dist/src/components/questionnaire.tsx +4 -0
  82. package/dist/src/components/radio-group.tsx +4 -0
  83. package/dist/src/components/resizable.tsx +7 -0
  84. package/dist/src/components/scroll-area.tsx +4 -0
  85. package/dist/src/components/select.tsx +7 -0
  86. package/dist/src/components/separator.tsx +5 -0
  87. package/dist/src/components/sheet.tsx +22 -0
  88. package/dist/src/components/sidebar.tsx +4 -0
  89. package/dist/src/components/skeleton.tsx +7 -0
  90. package/dist/src/components/slider.tsx +4 -0
  91. package/dist/src/components/spinner.tsx +6 -0
  92. package/dist/src/components/switch.tsx +7 -0
  93. package/dist/src/components/table.tsx +7 -0
  94. package/dist/src/components/tabs.tsx +7 -0
  95. package/dist/src/components/textarea.tsx +7 -0
  96. package/dist/src/components/toast.tsx +7 -0
  97. package/dist/src/components/toggle-group.tsx +4 -0
  98. package/dist/src/components/toggle.tsx +4 -0
  99. package/dist/src/components/tooltip.tsx +7 -0
  100. package/dist/src/composites/autocomplete.tsx +7 -0
  101. package/dist/src/composites/cascader.tsx +7 -0
  102. package/dist/src/composites/color-picker.tsx +7 -0
  103. package/dist/src/composites/confirm-dialog.tsx +7 -0
  104. package/dist/src/composites/copyable.tsx +9 -1
  105. package/dist/src/composites/currency-input.tsx +7 -0
  106. package/dist/src/composites/date-picker.tsx +7 -0
  107. package/dist/src/composites/date-range-picker.tsx +4 -0
  108. package/dist/src/composites/date-time-picker.tsx +7 -0
  109. package/dist/src/composites/date-time-range-picker.tsx +7 -0
  110. package/dist/src/composites/duration-input.tsx +7 -0
  111. package/dist/src/composites/mini-calendar.tsx +7 -0
  112. package/dist/src/composites/number-field.tsx +7 -0
  113. package/dist/src/composites/password-field.tsx +7 -0
  114. package/dist/src/composites/phone-input.tsx +7 -0
  115. package/dist/src/composites/rating.tsx +7 -0
  116. package/dist/src/composites/relative-date-picker.tsx +7 -0
  117. package/dist/src/composites/search-input.tsx +7 -0
  118. package/dist/src/composites/slider-range.tsx +7 -0
  119. package/dist/src/composites/tag-input.tsx +7 -0
  120. package/dist/src/composites/time-picker.tsx +11 -1
  121. package/dist/src/composites/time-range-picker.tsx +7 -0
  122. package/dist/src/composites/timezone-select.tsx +7 -0
  123. package/dist/src/composites/transfer.tsx +7 -0
  124. package/dist/src/composites/user-picker.tsx +7 -0
  125. package/dist/src/hooks/use-html-dark.ts +37 -0
  126. package/dist/src/hooks/use-mobile.ts +3 -0
  127. package/dist/src/i18n/en.ts +109 -2
  128. package/dist/src/i18n/zh.ts +99 -2
  129. package/dist/src/index.ts +147 -134
  130. package/dist/src/lib/clipboard.ts +10 -0
  131. package/dist/src/lib/illustrations.tsx +2 -2
  132. package/dist/src/products/code-block.tsx +35 -18
  133. package/dist/src/products/code-editor.tsx +156 -24
  134. package/dist/src/products/commit-graph.tsx +644 -0
  135. package/dist/src/products/data-grid.tsx +2 -1
  136. package/dist/src/products/diff-viewer.tsx +104 -8
  137. package/dist/src/products/event-calendar.tsx +28 -823
  138. package/dist/src/products/file-dropzone.tsx +7 -0
  139. package/dist/src/products/full-calendar/animations.ts +38 -0
  140. package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
  141. package/dist/src/products/full-calendar/constants.ts +10 -0
  142. package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
  143. package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
  144. package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
  145. package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
  146. package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
  147. package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
  148. package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
  149. package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
  150. package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
  151. package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
  152. package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
  153. package/dist/src/products/full-calendar/header/filter.tsx +58 -0
  154. package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
  155. package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
  156. package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
  157. package/dist/src/products/full-calendar/helpers.ts +453 -0
  158. package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
  159. package/dist/src/products/full-calendar/hooks.ts +70 -0
  160. package/dist/src/products/full-calendar/interfaces.ts +23 -0
  161. package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
  162. package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
  163. package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
  164. package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
  165. package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
  166. package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
  167. package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
  168. package/dist/src/products/full-calendar/types.ts +8 -0
  169. package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
  170. package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
  171. package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
  172. package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
  173. package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
  174. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
  175. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
  176. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
  177. package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
  178. package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
  179. package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
  180. package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
  181. package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
  182. package/dist/src/products/gantt.tsx +7 -0
  183. package/dist/src/products/jira-wiki.tsx +1 -0
  184. package/dist/src/products/jql-input.tsx +340 -37
  185. package/dist/src/products/json-viewer.tsx +7 -0
  186. package/dist/src/products/kanban-issue-panel.tsx +7 -0
  187. package/dist/src/products/kanban.tsx +1 -0
  188. package/dist/src/products/log-viewer.tsx +390 -18
  189. package/dist/src/products/markdown-editor.tsx +21 -6
  190. package/dist/src/products/markdown.tsx +7 -0
  191. package/dist/src/products/rich-text-editor.tsx +65 -59
  192. package/dist/src/products/sortable-list.tsx +7 -0
  193. package/dist/src/products/stepper.tsx +219 -70
  194. package/dist/src/products/timeline.tsx +7 -0
  195. package/dist/src/products/tree-view.tsx +7 -0
  196. package/dist/src/sdk/app-error-boundary.tsx +240 -0
  197. package/dist/src/sdk/app-id.ts +7 -0
  198. package/dist/src/sdk/use-app.ts +126 -0
  199. package/dist/src/styles/globals.css +1 -0
  200. package/dist/vendors/lodash.js +20 -0
  201. package/dist/vendors/motion.js +12 -0
  202. package/package.json +20 -19
  203. package/dist/src/products/jql-language.ts +0 -70
@@ -0,0 +1,161 @@
1
+ import { format, getYear, isSameDay, isSameMonth } from "date-fns";
2
+ import { useMemo } from "react";
3
+ import { motion } from "../../shims/motion";
4
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
5
+ import { useDateLocale } from "@monkey-mini-app/ui/i18n/context";
6
+ import { staggerContainer, transition } from "../../animations";
7
+ import { useCalendar } from "../../contexts/calendar-context";
8
+ import { EventListDialog } from "../../dialogs/events-list-dialog";
9
+ import { getCalendarCells } from "../../helpers";
10
+ import type { IEvent } from "../../interfaces";
11
+ import { EventBullet } from "../month-view/event-bullet";
12
+
13
+ interface IProps {
14
+ singleDayEvents: IEvent[];
15
+ multiDayEvents: IEvent[];
16
+ }
17
+
18
+ export function CalendarYearView({ singleDayEvents, multiDayEvents }: IProps) {
19
+ const { selectedDate, setSelectedDate } = useCalendar();
20
+ const dateLocale = useDateLocale();
21
+ const currentYear = getYear(selectedDate);
22
+ const allEvents = [...multiDayEvents, ...singleDayEvents];
23
+
24
+ const months = useMemo(
25
+ () =>
26
+ Array.from({ length: 12 }, (_, i) =>
27
+ format(new Date(2000, i, 1), "MMMM", { locale: dateLocale }),
28
+ ),
29
+ [dateLocale],
30
+ );
31
+
32
+ // Jan 2, 2000 was a Sunday — generate Sun..Sat short weekday labels.
33
+ const weekdays = useMemo(
34
+ () =>
35
+ Array.from({ length: 7 }, (_, i) =>
36
+ format(new Date(2000, 0, 2 + i), "EE", { locale: dateLocale }),
37
+ ),
38
+ [dateLocale],
39
+ );
40
+
41
+ return (
42
+ <div className="flex flex-col h-full overflow-y-auto p-4 sm:p-6">
43
+ {/* Year grid */}
44
+ <motion.div
45
+ initial="initial"
46
+ animate="animate"
47
+ variants={staggerContainer}
48
+ className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 auto-rows-fr"
49
+ >
50
+ {months.map((month, monthIndex) => {
51
+ const monthDate = new Date(currentYear, monthIndex, 1);
52
+ const cells = getCalendarCells(monthDate);
53
+
54
+ return (
55
+ <motion.div
56
+ key={month}
57
+ className="flex flex-col border border-border rounded-lg shadow-sm overflow-hidden"
58
+ initial={{ opacity: 0, scale: 0.95 }}
59
+ animate={{ opacity: 1, scale: 1 }}
60
+ transition={{ delay: monthIndex * 0.05, ...transition }}
61
+ aria-label={`${month} ${currentYear} calendar`}
62
+ >
63
+ {/* Month header */}
64
+ <button
65
+ type="button"
66
+ className="w-full px-3 py-2 text-center font-semibold text-sm sm:text-base cursor-pointer hover:bg-primary/20 transition-colors bg-transparent border-none appearance-none"
67
+ onClick={() =>
68
+ setSelectedDate(new Date(currentYear, monthIndex, 1))
69
+ }
70
+ aria-label={`Select ${month}`}
71
+ >
72
+ {month}
73
+ </button>
74
+
75
+ <div className="grid grid-cols-7 text-center text-xs font-medium text-muted-foreground py-2">
76
+ {weekdays.map((day) => (
77
+ <div key={day} className="p-1">
78
+ {day}
79
+ </div>
80
+ ))}
81
+ </div>
82
+
83
+ <div className="grid grid-cols-7 gap-0.5 p-1.5 flex-grow text-xs">
84
+ {cells.map((cell) => {
85
+ const isCurrentMonth = isSameMonth(cell.date, monthDate);
86
+ const isToday = isSameDay(cell.date, new Date());
87
+ const dayEvents = allEvents.filter((event) =>
88
+ isSameDay(new Date(event.startDate), cell.date),
89
+ );
90
+ const hasEvents = dayEvents.length > 0;
91
+
92
+ return (
93
+ <div
94
+ key={cell.date.toISOString()}
95
+ className={cn(
96
+ "flex flex-col items-center justify-start p-1 min-h-[2rem] relative",
97
+ !isCurrentMonth && "text-muted-foreground/40",
98
+ hasEvents && isCurrentMonth
99
+ ? "cursor-pointer hover:bg-accent/20 hover:rounded-md"
100
+ : "cursor-default",
101
+ )}
102
+ >
103
+ {isCurrentMonth && hasEvents ? (
104
+ <EventListDialog date={cell.date} events={dayEvents}>
105
+ <div className="w-full h-full flex flex-col items-center justify-start gap-0.5">
106
+ <span
107
+ className={cn(
108
+ "size-5 flex items-center justify-center font-medium",
109
+ isToday &&
110
+ "rounded-full bg-primary text-primary-foreground",
111
+ )}
112
+ >
113
+ {cell.day}
114
+ </span>
115
+ <div className="flex justify-center items-center gap-0.5">
116
+ {dayEvents.length <= 2 ? (
117
+ dayEvents
118
+ .slice(0, 2)
119
+ .map((event) => (
120
+ <EventBullet
121
+ key={event.id}
122
+ color={event.color}
123
+ className="size-1.5"
124
+ />
125
+ ))
126
+ ) : (
127
+ <div className="flex flex-col justify-center items-center">
128
+ <EventBullet
129
+ color={dayEvents[0].color}
130
+ className="size-1.5"
131
+ />
132
+ <span className="text-[0.6rem]">
133
+ +{dayEvents.length - 1}
134
+ </span>
135
+ </div>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </EventListDialog>
140
+ ) : (
141
+ <div className="w-full h-full flex flex-col items-center justify-start">
142
+ <span
143
+ className={cn(
144
+ "size-5 flex items-center justify-center font-medium",
145
+ )}
146
+ >
147
+ {cell.day}
148
+ </span>
149
+ </div>
150
+ )}
151
+ </div>
152
+ );
153
+ })}
154
+ </div>
155
+ </motion.div>
156
+ );
157
+ })}
158
+ </motion.div>
159
+ </div>
160
+ );
161
+ }
@@ -9,6 +9,13 @@ export type GanttTask = {
9
9
  end: Date
10
10
  }
11
11
 
12
+ /**
13
+ * Task bars on a date axis.
14
+ * @when Plans/schedules with start+end per row. Ordered events → `Timeline`.
15
+ * @example
16
+ * <Gantt tasks={[{ id: "t1", name: "联调", start: "2026-09-01", end: "2026-09-05" }]} />
17
+ * @family Calendar & date
18
+ */
12
19
  export function Gantt({ tasks }: { tasks: GanttTask[] }) {
13
20
  if (tasks.length === 0) {
14
21
  return <div data-testid="gantt" className="text-muted-foreground text-sm">No tasks</div>
@@ -250,6 +250,7 @@ function Blocks({
250
250
  * @when Jira description / comment body
251
251
  * @example
252
252
  * <JiraWiki classNames={{ heading: "text-base" }}>{`h2. Title\n* item`}</JiraWiki>
253
+ * @family Rich text
253
254
  */
254
255
  export function JiraWiki({
255
256
  children,
@@ -1,62 +1,365 @@
1
1
  "use client"
2
2
 
3
- import { oneDark } from "@codemirror/theme-one-dark"
4
- import { autocompletion } from "@codemirror/autocomplete"
5
- import CodeMirror from "@uiw/react-codemirror"
3
+ import { useEffect, useRef, useState } from "react"
6
4
 
7
- import { cn } from "@monkey-mini-app/ui/lib/utils"
5
+ import { useHtmlDark } from "@monkey-mini-app/ui/hooks/use-html-dark"
8
6
  import { useLabels } from "@monkey-mini-app/ui/i18n/context"
9
- import {
10
- defaultJqlFields,
11
- defaultJqlFunctions,
12
- jqlCompletion,
13
- jqlLanguage,
14
- type JqlSuggestItem,
15
- } from "./jql-language"
7
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
8
+
9
+ export type JqlSuggestItem = { name: string; type?: string }
10
+
11
+ /** JQL words that are never a field name or a value. */
12
+ const KEYWORDS = [
13
+ "AND",
14
+ "OR",
15
+ "NOT",
16
+ "IN",
17
+ "IS",
18
+ "EMPTY",
19
+ "NULL",
20
+ "WAS",
21
+ "CHANGED",
22
+ "ON",
23
+ "BY",
24
+ "ORDER",
25
+ "GROUP",
26
+ "ASC",
27
+ "DESC",
28
+ "BEFORE",
29
+ "AFTER",
30
+ "DURING",
31
+ "FOLLOWED",
32
+ "TO",
33
+ "FROM",
34
+ ]
35
+
36
+ const OPERATORS = ["=", "!=", ">", ">=", "<", "<=", "~", "!~"]
37
+
38
+ /** Words that start a clause, so `ORDER` + `BY` completes as one pair. */
39
+ const PHRASES = ["ORDER BY", "GROUP BY", "IS EMPTY", "IS NOT NULL", "FOLLOWED BY", "CHANGED ON", "WAS BEFORE", "IN ("]
16
40
 
41
+ const FUNCTIONS = [
42
+ "now()",
43
+ "startOfDay()",
44
+ "startOfWeek()",
45
+ "startOfMonth()",
46
+ "startOfQuarter()",
47
+ "startOfYear()",
48
+ "-1d",
49
+ "-7d",
50
+ "+2w",
51
+ ]
52
+
53
+ function bare<T extends object>(m: T & { default?: T }): T {
54
+ return (m.default ?? m) as T
55
+ }
56
+
57
+ type CmCore = {
58
+ EditorView: any
59
+ keymap: any
60
+ placeholder: any
61
+ syntaxHighlighting: any
62
+ HighlightStyle: any
63
+ StreamLanguage: any
64
+ autocompletion: any
65
+ completionKeymap: any
66
+ tags: any
67
+ EditorState: any
68
+ }
69
+
70
+ /** CodeMirror 6 core, loaded once per page (esm.sh, never npm). */
71
+ let corePromise: Promise<CmCore> | null = null
72
+ function loadCore(): Promise<CmCore> {
73
+ corePromise ??= (async () => {
74
+ const [viewMod, stateMod, langMod, acMod, tagsMod] = await Promise.all([
75
+ import("https://esm.sh/@codemirror/view@6"),
76
+ import("https://esm.sh/@codemirror/state@6"),
77
+ import("https://esm.sh/@codemirror/language@6"),
78
+ import("https://esm.sh/@codemirror/autocomplete@6"),
79
+ import("https://esm.sh/@lezer/highlight@1"),
80
+ ])
81
+ const view = bare(viewMod as any)
82
+ const state = bare(stateMod as any)
83
+ const lang = bare(langMod as any)
84
+ const ac = bare(acMod as any)
85
+ const hl = bare(tagsMod as any)
86
+ return {
87
+ EditorView: view.EditorView,
88
+ keymap: view.keymap,
89
+ placeholder: view.placeholder,
90
+ EditorState: state.EditorState,
91
+ syntaxHighlighting: lang.syntaxHighlighting,
92
+ HighlightStyle: lang.HighlightStyle,
93
+ StreamLanguage: lang.StreamLanguage,
94
+ autocompletion: ac.autocompletion,
95
+ completionKeymap: ac.completionKeymap,
96
+ tags: hl.tags,
97
+ }
98
+ })().catch((err) => {
99
+ corePromise = null
100
+ throw err
101
+ })
102
+ return corePromise
103
+ }
104
+
105
+ /**
106
+ * Stream tokenizer: `"quoted"` / `'quoted'`, numbers, dates, relative intervals,
107
+ * operators, keywords, then a word is a **field** when an operator follows it.
108
+ */
17
109
  /**
18
- * CodeMirror JQL editor with field/function completion (host injects fields).
19
- * @when Issue search / saved filters
110
+ * The subset of CodeMirror's `StringStream` this tokenizer touches. `StreamLanguage` arrives
111
+ * as `any` (the module is loaded from a CDN at runtime), so without this the `token`
112
+ * parameter silently degrades to an implicit any and the guard calls go unchecked.
113
+ */
114
+ type TokenStream = {
115
+ eatSpace(): boolean
116
+ next(): string | false
117
+ match(re: RegExp, consume?: boolean): RegExpMatchArray | null
118
+ }
119
+
120
+ function jqlLanguage(core: CmCore) {
121
+ const kw = new Set(KEYWORDS)
122
+ return core.StreamLanguage.define({
123
+ name: "jql",
124
+ token(stream: TokenStream) {
125
+ if (stream.eatSpace()) return null
126
+ if (stream.match(/^'[^']*'/) || stream.match(/^"[^"]*"/)) return "string"
127
+ if (stream.match(/^[+-]?\d+(h|d|w|mo|q|y)\b/)) return "number"
128
+ if (stream.match(/^\d{4}-\d{2}-\d{2}/)) return "number"
129
+ if (stream.match(/^[+-]?\d+(\.\d+)?/)) return "number"
130
+ if (stream.match(/^(!=|>=|<=|!~|=|>|<|~)/)) return "operator"
131
+ if (stream.match(/^[,()[\]]/)) return "operator"
132
+ const word = stream.match(/^[A-Za-z_][\w.()-]*/)
133
+ if (word) {
134
+ const raw = String(word[0])
135
+ if (kw.has(raw.toUpperCase())) return "keyword"
136
+ const followedByOp =
137
+ stream.match(/^\s*(=|!=|>=|<=|>|<|~|!~)/, false) ||
138
+ stream.match(/^\s*(IN|IS|WAS|CHANGED|FOLLOWED|ORDER|GROUP)\b/i, false)
139
+ return followedByOp ? "property" : "value"
140
+ }
141
+ if (stream.match(/^[^\s]+/)) return "value"
142
+ stream.next()
143
+ return null
144
+ },
145
+ })
146
+ }
147
+
148
+ /** Token classes are coloured from CSS vars so light/dark and palettes apply. */
149
+ function jqlHighlight(core: CmCore) {
150
+ const { tags } = core
151
+ return core.HighlightStyle.define([
152
+ { tag: tags.keyword, class: "cm-jql-kw" },
153
+ { tag: tags.propertyName, class: "cm-jql-prop" },
154
+ { tag: tags.string, class: "cm-jql-str" },
155
+ { tag: tags.number, class: "cm-jql-num" },
156
+ { tag: tags.operator, class: "cm-jql-op" },
157
+ ])
158
+ }
159
+
160
+ function jqlTheme(core: CmCore, dark: boolean) {
161
+ return core.EditorView.theme({
162
+ "&": { backgroundColor: "transparent", color: "var(--foreground)", height: "100%" },
163
+ ".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono, ui-monospace, monospace)" },
164
+ ".cm-content": { caretColor: "var(--foreground)", padding: "8px 12px" },
165
+ ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--foreground)" },
166
+ ".cm-activeLine": { backgroundColor: "transparent" },
167
+ ".cm-selectionBackground, ::selection": {
168
+ backgroundColor: dark ? "rgba(99,130,255,.32)" : "color-mix(in oklch, var(--primary) 22%, transparent)",
169
+ },
170
+ ".cm-jql-kw": { color: "var(--primary)", fontWeight: "600" },
171
+ ".cm-jql-prop": { color: "var(--foreground)", fontWeight: "600" },
172
+ ".cm-jql-str": { color: "var(--chart-2)" },
173
+ ".cm-jql-num": { color: "var(--chart-3)" },
174
+ ".cm-jql-op": { color: "var(--muted-foreground)" },
175
+ // Completion pop-up follows the panel tokens instead of CM's defaults.
176
+ ".cm-tooltip": {
177
+ backgroundColor: "var(--popover)",
178
+ border: "1px solid var(--border)",
179
+ borderRadius: "10px",
180
+ color: "var(--popover-foreground)",
181
+ fontSize: "12px",
182
+ },
183
+ ".cm-tooltip-autocomplete ul li[aria-selected]": {
184
+ backgroundColor: "color-mix(in oklch, var(--primary) 18%, transparent)",
185
+ color: "var(--popover-foreground)",
186
+ },
187
+ })
188
+ }
189
+
190
+ type Opt = { label: string; detail?: string; type?: string; boost?: number }
191
+
192
+ function optionFor(name: string, kind: string): Opt {
193
+ return { label: name, type: kind }
194
+ }
195
+
196
+ /**
197
+ * Field names → operators → values, based on what precedes the cursor.
198
+ */
199
+ function complete(fields: JqlSuggestItem[]) {
200
+ const fieldNames = fields.map((f) => f.name)
201
+ const byName = new Map(fields.map((f) => [f.name, f]))
202
+ const WORD = /([\w."'-]*)$/
203
+
204
+ return (ctx: any) => {
205
+ const before = ctx.state.doc.sliceString(0, ctx.pos)
206
+ const word = String(WORD.exec(before)?.[0] ?? "")
207
+ const from = ctx.pos - word.length
208
+ const clause = before.slice(before.lastIndexOf("\n") + 1)
209
+ const options: Opt[] = []
210
+
211
+ // `field <op> ` → values for that field (plus functions and phrases).
212
+ const assign = /([\w.]+)\s*(=|!=|>=|<=|>|<|~|!~|IS|IN|WAS)\s*$/i.exec(clause)
213
+ if (assign) {
214
+ const name = assign[1]
215
+ if (byName.has(name)) {
216
+ // quoted literal first, then the values worth trying for this field
217
+ options.push({ label: '\"...\"', detail: "literal", type: "text", boost: 999 })
218
+ for (const v of sampleValues(name)) options.push({ label: v, detail: name, type: "value", boost: 500 })
219
+ }
220
+ for (const fn of FUNCTIONS) options.push({ label: fn, detail: "jql value", type: "function", boost: 400 })
221
+ return { from, options, validFor: /^[\w."'-]*$/ }
222
+ }
223
+
224
+ // Just typed a field name → offer operators.
225
+ if (/([\w.]+)\s*$/i.test(clause) && !/\s(and|or|not|by|in|is|was)$/i.test(clause)) {
226
+ for (const op of OPERATORS) options.push({ label: op, type: "operator", boost: 600 })
227
+ }
228
+
229
+ // Otherwise: fields and clause keywords.
230
+ for (const f of fieldNames) options.push(optionFor(f, "property"))
231
+ for (const p of PHRASES) options.push({ label: p, type: "keyword", boost: -100 })
232
+ for (const k of KEYWORDS) options.push({ label: k, type: "keyword", boost: -200 })
233
+ return { from, options, validFor: /^[\w."'-]*$/ }
234
+ }
235
+ }
236
+
237
+ /** Values worth offering per field — real installs would fetch these from the backend. */
238
+ function sampleValues(field: string): string[] {
239
+ const table: Record<string, string[]> = {
240
+ status: ["Open", "In Progress", "Blocked", "Done"],
241
+ assignee: ["currentUser()", "pwang12", "unassigned"],
242
+ reporter: ["currentUser()"],
243
+ priority: ["Blocker", "Critical", "Major", "Minor", "Trivial"],
244
+ resolution: ["Done", "Won't Do", "Duplicate"],
245
+ issuetype: ["Bug", "Task", "Story", "Sub-task"],
246
+ labels: ["cn", "stg", "hotfix"],
247
+ project: ["TMS", "GLS", "LMA"],
248
+ }
249
+ return table[field.toLowerCase()] ?? []
250
+ }
251
+
252
+ /**
253
+ * JQL query field: CodeMirror 6 (esm.sh, on demand) with syntax highlighting and
254
+ * field/operator/value completion that opens as you type. Falls back to a plain
255
+ * textarea when the CDN is unreachable — the query still edits.
256
+ * @when Issue search / saved filters. Needs a real field list: pass `fields`.
20
257
  * @example
21
- * <JqlInput value={jql} onChange={setJql} fields={[{ name: "status" }]} />
258
+ * <JqlInput value={jql} onChange={setJql} fields={[{ name: "status" }, { name: "assignee" }]} />
259
+ * @family Form
22
260
  */
23
261
  export function JqlInput({
24
262
  value,
25
263
  onChange,
26
- fields = defaultJqlFields,
27
- functions = defaultJqlFunctions,
28
- height = "88px",
264
+ fields = [],
29
265
  className,
266
+ height = "88px",
30
267
  }: {
31
268
  value: string
32
269
  onChange?: (value: string) => void
270
+ /** Field names offered by completion (from your JQL backend). */
33
271
  fields?: JqlSuggestItem[]
34
- functions?: JqlSuggestItem[]
35
- height?: string
36
272
  className?: string
273
+ height?: string
37
274
  }) {
38
275
  const t = useLabels("jqlInput")
39
- const dark =
40
- typeof document !== "undefined" &&
41
- document.documentElement.classList.contains("dark")
276
+ const hostRef = useRef<HTMLDivElement>(null)
277
+ const viewRef = useRef<any>(null)
278
+ const onChangeRef = useRef(onChange)
279
+ onChangeRef.current = onChange
280
+ const valueRef = useRef(value)
281
+ valueRef.current = value
282
+ const fieldsRef = useRef(fields)
283
+ fieldsRef.current = fields
284
+ const [cm, setCm] = useState(false)
285
+ const dark = useHtmlDark()
286
+
287
+ useEffect(() => {
288
+ if (!hostRef.current) return
289
+ let cancelled = false
290
+ ;(async () => {
291
+ try {
292
+ const core = await loadCore()
293
+ if (cancelled || !hostRef.current) return
294
+ const view = new core.EditorView({
295
+ parent: hostRef.current,
296
+ doc: valueRef.current,
297
+ extensions: [
298
+ core.EditorView.lineWrapping,
299
+ jqlLanguage(core),
300
+ core.syntaxHighlighting(jqlHighlight(core)),
301
+ core.EditorView.darkTheme.of(dark),
302
+ jqlTheme(core, dark),
303
+ core.placeholder(t.placeholder),
304
+ core.autocompletion({
305
+ activateOnTyping: true,
306
+ icons: false,
307
+ defaultTable: undefined,
308
+ override: [complete(fieldsRef.current)],
309
+ }),
310
+ core.keymap.of(core.completionKeymap),
311
+ core.EditorView.updateListener.of((u: any) => {
312
+ if (u.docChanged) onChangeRef.current?.(view.state.doc.toString())
313
+ }),
314
+ ],
315
+ })
316
+ viewRef.current = view
317
+ setCm(true)
318
+ } catch (err) {
319
+ // CDN down or build failure: the textarea fallback still edits the query.
320
+ console.warn("[JqlInput] CodeMirror unavailable, using plain textarea", err)
321
+ }
322
+ })()
323
+ return () => {
324
+ cancelled = true
325
+ viewRef.current?.destroy()
326
+ viewRef.current = null
327
+ }
328
+ // `fields` is read through a ref so a new list never remounts the editor.
329
+ }, [dark, t.placeholder])
330
+
331
+ useEffect(() => {
332
+ const view = viewRef.current
333
+ if (!view) return
334
+ const cur = view.state.doc.toString()
335
+ if (value === cur) return
336
+ view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
337
+ }, [value])
42
338
 
43
339
  return (
44
- <div data-testid="jql-input" className={cn("overflow-hidden rounded-xl border", className)}>
45
- <CodeMirror
46
- value={value}
47
- height={height}
48
- theme={dark ? oneDark : "light"}
49
- placeholder={t.placeholder}
50
- extensions={[
51
- jqlLanguage,
52
- autocompletion({ override: [jqlCompletion(fields, functions)] }),
53
- ]}
54
- onChange={(next) => onChange?.(next)}
55
- basicSetup={{ lineNumbers: false, foldGutter: false }}
340
+ <div
341
+ className={cn(
342
+ "overflow-hidden rounded-xl border border-border bg-card",
343
+ className,
344
+ )}
345
+ >
346
+ {!cm && (
347
+ <textarea
348
+ data-testid="jql-input"
349
+ value={value}
350
+ onChange={(e) => onChange?.(e.target.value)}
351
+ placeholder={t.placeholder}
352
+ spellCheck={false}
353
+ className="w-full resize-y overflow-auto bg-transparent px-3 py-2 font-mono text-xs leading-6 outline-none"
354
+ style={{ height }}
355
+ />
356
+ )}
357
+ <div
358
+ ref={hostRef}
359
+ data-testid={cm ? "jql-input" : undefined}
360
+ className={cn("[&_.cm-editor]:h-full [&_.cm-editor]:outline-none", !cm && "hidden")}
361
+ style={{ height: cm ? height : undefined, minHeight: cm ? height : 0 }}
56
362
  />
57
363
  </div>
58
364
  )
59
365
  }
60
-
61
- export { defaultJqlFields, defaultJqlFunctions } from "./jql-language"
62
- export type { JqlSuggestItem }
@@ -28,6 +28,13 @@ function Node({ label, value, depth }: { label?: string; value: unknown; depth:
28
28
  )
29
29
  }
30
30
 
31
+ /**
32
+ * Collapsible JSON tree.
33
+ * @when API responses/config. Plain text with wrapping → `CodeBlock`.
34
+ * @example
35
+ * <JsonViewer value={data} />
36
+ * @family Discovery & inspect
37
+ */
31
38
  export function JsonViewer({ value }: { value: unknown }) {
32
39
  return (
33
40
  <div data-testid="json-viewer" className="rounded-xl border bg-card p-3">
@@ -17,6 +17,13 @@ import { StatusBadge } from "@monkey-mini-app/ui/blocks/status-badge"
17
17
  import type { KanbanCard } from "@monkey-mini-app/ui/products/kanban"
18
18
  import { useLabels } from "@monkey-mini-app/ui/i18n/context"
19
19
 
20
+ /**
21
+ * Slide-over detail panel for a `Kanban` card.
22
+ * @when Board + click-to-edit issue. Pair with `Kanban`'s selected card.
23
+ * @example
24
+ * <KanbanIssuePanel card={card} open={open} onOpenChange={setOpen} />
25
+ * @family Surface
26
+ */
20
27
  export function KanbanIssuePanel({
21
28
  card,
22
29
  columnTitle,
@@ -192,6 +192,7 @@ function Column({
192
192
  * @when Issue/workflow boards
193
193
  * @example
194
194
  * <Kanban columns={cols} cards={cards} onCardsChange={setCards} onCardClick={open} />
195
+ * @family Data & tables
195
196
  */
196
197
  export function Kanban({
197
198
  columns,