@unabridged/midwest 0.22.1 → 0.23.1

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 (31) hide show
  1. package/README.md +1 -1
  2. package/app/assets/javascript/midwest/index.ts +8 -0
  3. package/app/assets/javascript/midwest/keyboard_navigation.ts +68 -0
  4. package/app/assets/javascript/midwest/markdown.ts +79 -0
  5. package/app/assets/javascript/midwest.js +795 -39
  6. package/app/assets/javascript/midwest.js.map +1 -1
  7. package/app/assets/stylesheets/midwest/utilities.css +4 -0
  8. package/app/assets/stylesheets/midwest.css +1 -1
  9. package/app/assets/stylesheets/midwest.tailwind.css +13 -1
  10. package/dist/css/midwest.css +1 -1
  11. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
  12. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  13. package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js +42 -0
  14. package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js.map +1 -0
  15. package/dist/javascript/collection/app/assets/javascript/midwest/markdown.js +52 -0
  16. package/dist/javascript/collection/app/assets/javascript/midwest/markdown.js.map +1 -0
  17. package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js +270 -2
  18. package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js.map +1 -1
  19. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +8 -37
  20. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
  21. package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js +83 -0
  22. package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js.map +1 -0
  23. package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js +42 -0
  24. package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js.map +1 -0
  25. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +43 -0
  26. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -0
  27. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +273 -0
  28. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -0
  29. package/dist/javascript/midwest.js +795 -39
  30. package/dist/javascript/midwest.js.map +1 -1
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Rails Tests](https://github.com/unabridged/midwest/actions/workflows/rails-tests.yml/badge.svg?branch=main)](https://github.com/unabridged/midwest/actions/workflows/rails-tests.yml)
4
4
  [![Bridgetown Tests](https://github.com/unabridged/midwest/actions/workflows/bridgetown-tests.yml/badge.svg?branch=main)](https://github.com/unabridged/midwest/actions/workflows/bridgetown-tests.yml)
5
5
  [![Ruby](https://img.shields.io/badge/Ruby-3.3_%7C_3.4-CC342D?logo=ruby&logoColor=white)](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
6
- [![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen)](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
6
+ [![Coverage](https://img.shields.io/badge/coverage-96%25-brightgreen)](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
7
7
 
8
8
  Midwest Component Library is an implementation of the Midwest Design System using [ViewComponent](https://github.com/github/view_component).
9
9
 
@@ -32,6 +32,10 @@ import PasswordRequirements from '../../../components/midwest/password_requireme
32
32
  import CodeBlock from '../../../components/midwest/code_block_component/code_block_component_controller'
33
33
  import DatePicker from '../../../components/midwest/form/date_picker_component/date_picker_component_controller'
34
34
  import TimePicker from '../../../components/midwest/form/time_picker_component/time_picker_component_controller'
35
+ import IntersectionObserver from '../../../components/midwest/intersection_observer_component/intersection_observer_component_controller'
36
+ import FocusTrap from '../../../components/midwest/focus_trap_component/focus_trap_component_controller'
37
+ import Onboarding from '../../../components/midwest/onboarding_component/onboarding_component_controller'
38
+ import ThemeSwitch from '../../../components/midwest/form/switch_component/theme_switch_controller'
35
39
  /* IMPORTS */
36
40
 
37
41
  export { Card, Banner, CountdownTimer, Chart }
@@ -71,5 +75,9 @@ export function registerMidwestControllers (application: any) {
71
75
  application.register('midwest-code-block', CodeBlock)
72
76
  application.register('midwest-date-picker', DatePicker)
73
77
  application.register('midwest-time-picker', TimePicker)
78
+ application.register('midwest-intersection-observer', IntersectionObserver)
79
+ application.register('midwest-focus-trap', FocusTrap)
80
+ application.register('midwest-onboarding', Onboarding)
81
+ application.register('midwest-theme-switch', ThemeSwitch)
74
82
  /* EXPORTS */
75
83
  }
@@ -0,0 +1,68 @@
1
+ // Shared keyboard-navigation helpers for menu/list components.
2
+ //
3
+ // Implements the WAI-ARIA "roving focus" pattern: arrow keys move DOM focus
4
+ // between a flat list of items, Home/End jump to the ends, and Escape/Tab hand
5
+ // control back to the caller (typically to close a popover and restore focus).
6
+ // Components own *which* elements are navigable and *what* closing means; this
7
+ // module owns the key handling so the behavior stays consistent across the
8
+ // library.
9
+
10
+ const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape', 'Tab']
11
+
12
+ export interface RovingFocusOptions {
13
+ // Called on Escape. Typically closes the menu and returns focus to the trigger.
14
+ onEscape?: () => void
15
+ // Called on Tab. The default Tab behavior is left intact so focus still leaves
16
+ // the menu; use this to close the menu alongside it.
17
+ onTab?: () => void
18
+ // When true (default) arrow navigation wraps from the last item to the first
19
+ // and vice versa. When false it stops at the ends.
20
+ loop?: boolean
21
+ }
22
+
23
+ // Handles a keydown against a roving-focus list. Returns true when the event
24
+ // was one this helper is responsible for, so callers can `stopPropagation()`
25
+ // only for keys that were actually handled.
26
+ export function handleRovingFocusKeydown (
27
+ event: KeyboardEvent,
28
+ items: HTMLElement[],
29
+ options: RovingFocusOptions = {}
30
+ ): boolean {
31
+ if (items.length === 0) return false
32
+ if (!NAVIGATION_KEYS.includes(event.key)) return false
33
+
34
+ const { onEscape, onTab, loop = true } = options
35
+ const current = items.findIndex(item => item === document.activeElement)
36
+
37
+ switch (event.key) {
38
+ case 'ArrowDown': {
39
+ event.preventDefault()
40
+ const next = current < items.length - 1 ? current + 1 : (loop ? 0 : items.length - 1)
41
+ items[next].focus()
42
+ break
43
+ }
44
+ case 'ArrowUp': {
45
+ event.preventDefault()
46
+ const prev = current > 0 ? current - 1 : (loop ? items.length - 1 : 0)
47
+ items[prev].focus()
48
+ break
49
+ }
50
+ case 'Home':
51
+ event.preventDefault()
52
+ items[0].focus()
53
+ break
54
+ case 'End':
55
+ event.preventDefault()
56
+ items[items.length - 1].focus()
57
+ break
58
+ case 'Escape':
59
+ event.preventDefault()
60
+ onEscape?.()
61
+ break
62
+ case 'Tab':
63
+ onTab?.()
64
+ break
65
+ }
66
+
67
+ return true
68
+ }
@@ -0,0 +1,79 @@
1
+ // Tiny, dependency-free Markdown renderer for streamed chat responses.
2
+ //
3
+ // It is intentionally a small subset (paragraphs, lists, fenced/inline code,
4
+ // bold, italic, and safe links) rather than a full CommonMark implementation —
5
+ // the command palette streams tokens and cannot afford a heavy parser.
6
+ //
7
+ // Security: model output is untrusted. Text is HTML-escaped BEFORE any markup is
8
+ // applied, and links are restricted to http(s)/mailto, so the result is safe to
9
+ // assign to innerHTML.
10
+
11
+ const PLACEHOLDER = '\uE000' // private-use char, unlikely in real text
12
+
13
+ export function renderMarkdown (src: string): string {
14
+ const blocks: string[] = []
15
+
16
+ // Pull fenced code blocks out first so their contents are never treated as markup.
17
+ const withoutFences = src.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code: string) => {
18
+ const body = escapeHtml(code.replace(/\n$/, ''))
19
+ blocks.push(`<pre class="command-palette-code"><code>${body}</code></pre>`)
20
+ return `${PLACEHOLDER}${blocks.length - 1}${PLACEHOLDER}`
21
+ })
22
+
23
+ const lines = escapeHtml(withoutFences).split('\n')
24
+ const out: string[] = []
25
+ let listOpen = false
26
+
27
+ const closeList = () => {
28
+ if (listOpen) {
29
+ out.push('</ul>')
30
+ listOpen = false
31
+ }
32
+ }
33
+
34
+ for (const line of lines) {
35
+ const placeholder = line.match(new RegExp(`^${PLACEHOLDER}(\\d+)${PLACEHOLDER}$`))
36
+ if (placeholder) {
37
+ closeList()
38
+ out.push(line)
39
+ continue
40
+ }
41
+
42
+ const item = line.match(/^\s*[-*]\s+(.*)$/)
43
+ if (item) {
44
+ if (!listOpen) {
45
+ out.push('<ul>')
46
+ listOpen = true
47
+ }
48
+ out.push(`<li>${inline(item[1])}</li>`)
49
+ continue
50
+ }
51
+
52
+ closeList()
53
+ if (line.trim() !== '') out.push(`<p>${inline(line)}</p>`)
54
+ }
55
+
56
+ closeList()
57
+
58
+ return out
59
+ .join('\n')
60
+ .replace(new RegExp(`${PLACEHOLDER}(\\d+)${PLACEHOLDER}`, 'g'), (_match, index: string) => blocks[Number(index)])
61
+ }
62
+
63
+ function inline (text: string): string {
64
+ return text
65
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
66
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
67
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>')
68
+ .replace(
69
+ /\[([^\]]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g,
70
+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
71
+ )
72
+ }
73
+
74
+ function escapeHtml (text: string): string {
75
+ return text
76
+ .replace(/&/g, '&amp;')
77
+ .replace(/</g, '&lt;')
78
+ .replace(/>/g, '&gt;')
79
+ }