rip-lang 3.13.71 → 3.13.73

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,28 @@
1
+ # InputGroup — accessible headless input with prefix/suffix
2
+ #
3
+ # Wraps a form control with optional prefix and suffix elements.
4
+ # Use $prefix and $suffix on children to mark addon positions.
5
+ # Tracks child input focus for styling. Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # InputGroup
9
+ # span $prefix: true, "$"
10
+ # Input value <=> amount, type: "number"
11
+ # InputGroup
12
+ # Input value <=> search, placeholder: "Search..."
13
+ # button $suffix: true, @click: doSearch, "Go"
14
+
15
+ export InputGroup = component
16
+ @disabled := false
17
+
18
+ focused := false
19
+
20
+ mounted: ->
21
+ ctrl = @_root?.querySelector('input, select, textarea')
22
+ return unless ctrl
23
+ ctrl.addEventListener 'focusin', => focused = true
24
+ ctrl.addEventListener 'focusout', => focused = false
25
+
26
+ render
27
+ div ref: "_root", $disabled: @disabled?!, $focused: focused?!
28
+ slot
@@ -0,0 +1,16 @@
1
+ # Label — accessible headless form label
2
+ #
3
+ # Standalone label element that associates with a form control via @for.
4
+ # Companions Field but works independently. Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # Label for: "email-input", "Email address"
8
+ # Label required: true, "Username"
9
+
10
+ export Label = component
11
+ @for := null
12
+ @required := false
13
+
14
+ render
15
+ label for: @for?!, $required: @required?!
16
+ slot
@@ -0,0 +1,32 @@
1
+ # NativeSelect — accessible headless native select wrapper
2
+ #
3
+ # Wraps a native <select> element with state tracking via data attributes.
4
+ # Use when the browser's built-in dropdown is preferred. Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # NativeSelect value <=> role, @change: handleChange
8
+ # option value: "", "Choose a role..."
9
+ # option value: "admin", "Admin"
10
+ # option value: "user", "User"
11
+
12
+ export NativeSelect = component
13
+ @value := ''
14
+ @disabled := false
15
+ @required := false
16
+
17
+ focused := false
18
+
19
+ onChange: (e) ->
20
+ @value = e.target.value
21
+ @emit 'change', @value
22
+
23
+ render
24
+ select value: @value, disabled: @disabled, required: @required
25
+ aria-disabled: @disabled?!
26
+ aria-required: @required?!
27
+ $disabled: @disabled?!
28
+ $focused: focused?!
29
+ @change: @onChange
30
+ @focusin: (=> focused = true)
31
+ @focusout: (=> focused = false)
32
+ slot
@@ -0,0 +1,123 @@
1
+ # Pagination — accessible headless page navigation
2
+ #
3
+ # Renders page buttons with prev/next and ellipsis gaps.
4
+ # Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # Pagination page <=> currentPage, total: 100, perPage: 10
8
+ # Pagination page <=> currentPage, total: 500, perPage: 20, siblingCount: 2
9
+
10
+ export Pagination = component
11
+ @page := 1
12
+ @total := 0
13
+ @perPage := 10
14
+ @siblingCount := 1
15
+
16
+ totalPages ~= Math.max(1, Math.ceil(@total / @perPage))
17
+ _ready := false
18
+
19
+ _range: (start, fin) ->
20
+ len = fin - start + 1
21
+ Array.from {length: len}, (_, i) -> start + i
22
+
23
+ visiblePages ~=
24
+ tp = totalPages
25
+ sibs = @siblingCount
26
+ current = @page
27
+
28
+ totalNumbers = sibs * 2 + 5
29
+ return @_range(1, tp) if tp <= totalNumbers
30
+
31
+ leftSib = Math.max(current - sibs, 1)
32
+ rightSib = Math.min(current + sibs, tp)
33
+
34
+ showLeftDots = leftSib > 2
35
+ showRightDots = rightSib < tp - 1
36
+
37
+ if not showLeftDots and showRightDots
38
+ leftCount = 3 + 2 * sibs
39
+ leftRange = @_range(1, leftCount)
40
+ return [...leftRange, -1, tp]
41
+
42
+ if showLeftDots and not showRightDots
43
+ rightCount = 3 + 2 * sibs
44
+ rightRange = @_range(tp - rightCount + 1, tp)
45
+ return [1, -2, ...rightRange]
46
+
47
+ midRange = @_range(leftSib, rightSib)
48
+ [1, -2, ...midRange, -1, tp]
49
+
50
+ goto: (pg) ->
51
+ pg = Math.max(1, Math.min(pg, totalPages))
52
+ return if pg is @page
53
+ @page = pg
54
+ @emit 'change', @page
55
+
56
+ onKeydown: (e) ->
57
+ switch e.key
58
+ when 'ArrowLeft'
59
+ e.preventDefault()
60
+ @goto(@page - 1)
61
+ when 'ArrowRight'
62
+ e.preventDefault()
63
+ @goto(@page + 1)
64
+ when 'Home'
65
+ e.preventDefault()
66
+ @goto(1)
67
+ when 'End'
68
+ e.preventDefault()
69
+ @goto(totalPages)
70
+
71
+ mounted: ->
72
+ _ready = true
73
+
74
+ _prevPages = null
75
+
76
+ _rebuild: (inner) ->
77
+ frag = document.createDocumentFragment()
78
+ for pg in visiblePages
79
+ if pg < 0
80
+ el = document.createElement 'span'
81
+ el.setAttribute 'data-ellipsis', ''
82
+ el.textContent = '...'
83
+ else
84
+ el = document.createElement 'button'
85
+ el.setAttribute 'aria-label', "Page #{pg}"
86
+ el.setAttribute 'data-page', ''
87
+ el.textContent = "#{pg}"
88
+ el.addEventListener 'click', => @goto(pg)
89
+ frag.appendChild el
90
+ inner.replaceChildren frag
91
+ _prevPages = visiblePages.join ','
92
+
93
+ _syncActive: (inner) ->
94
+ for btn in inner.querySelectorAll('[data-page]')
95
+ pg = parseInt btn.textContent
96
+ if pg is @page
97
+ btn.setAttribute 'aria-current', 'page'
98
+ btn.setAttribute 'data-active', ''
99
+ else
100
+ btn.removeAttribute 'aria-current'
101
+ btn.removeAttribute 'data-active'
102
+
103
+ ~>
104
+ return unless _ready
105
+ inner = @_nav?.querySelector('[data-pages]')
106
+ return unless inner
107
+
108
+ key = visiblePages.join ','
109
+ if key isnt _prevPages
110
+ @_rebuild inner
111
+ @_syncActive inner
112
+
113
+ render
114
+ nav ref: "_nav", aria-label: "Pagination", @keydown: @onKeydown
115
+ button $prev: true, aria-label: "Previous page"
116
+ disabled: @page <= 1
117
+ $disabled: (@page <= 1)?!
118
+ @click: (=> @goto(@page - 1))
119
+ . $pages: true
120
+ button $next: true, aria-label: "Next page"
121
+ disabled: @page >= totalPages
122
+ $disabled: (@page >= totalPages)?!
123
+ @click: (=> @goto(@page + 1))
@@ -0,0 +1,123 @@
1
+ # Resizable — accessible headless resizable panels
2
+ #
3
+ # Container with draggable handles between panels for resizing.
4
+ # Panel sizes are stored as percentages and exposed via CSS
5
+ # custom properties. Place [data-handle] elements between [data-panel]
6
+ # elements. Ships zero CSS.
7
+ #
8
+ # Usage:
9
+ # Resizable
10
+ # div $panel: true
11
+ # p "Left panel"
12
+ # div $handle: true
13
+ # div $panel: true
14
+ # p "Right panel"
15
+ #
16
+ # Resizable orientation: "vertical", minSize: 20
17
+ # div $panel: true, "Top"
18
+ # div $handle: true
19
+ # div $panel: true, "Bottom"
20
+
21
+ export Resizable = component
22
+ @orientation := 'horizontal'
23
+ @minSize := 10
24
+ @maxSize := 90
25
+
26
+ _ready := false
27
+ _dragging = null
28
+ _startPos = 0
29
+ _startSizes = []
30
+ sizes := []
31
+
32
+ _panels: ->
33
+ return [] unless @_root
34
+ Array.from(@_root.querySelectorAll(':scope > [data-panel]') or [])
35
+
36
+ _handles: ->
37
+ return [] unless @_root
38
+ Array.from(@_root.querySelectorAll(':scope > [data-handle]') or [])
39
+
40
+ mounted: ->
41
+ _ready = true
42
+ panels = @_panels()
43
+ count = panels.length
44
+ if count and not sizes.length
45
+ even = 100 / count
46
+ sizes = Array.from {length: count}, -> even
47
+
48
+ @_handles().forEach (handle, idx) =>
49
+ handle.setAttribute 'role', 'separator'
50
+ handle.setAttribute 'tabindex', '0'
51
+ handle.addEventListener 'pointerdown', (e) => @_onPointerDown(idx, e)
52
+ handle.addEventListener 'keydown', (e) => @_onKeydown(idx, e)
53
+
54
+ _getPos: (e) ->
55
+ if @orientation is 'horizontal' then e.clientX else e.clientY
56
+
57
+ _getContainerSize: ->
58
+ rect = @_root?.getBoundingClientRect()
59
+ return 0 unless rect
60
+ if @orientation is 'horizontal' then rect.width else rect.height
61
+
62
+ _onPointerDown: (handleIdx, e) ->
63
+ e.preventDefault()
64
+ _dragging = handleIdx
65
+ _startPos = @_getPos(e)
66
+ _startSizes = [...sizes]
67
+ e.target.setPointerCapture(e.pointerId)
68
+ e.target.toggleAttribute 'data-dragging', true
69
+
70
+ _onPointerMove: (e) ->
71
+ return unless _dragging?
72
+ total = @_getContainerSize()
73
+ return unless total
74
+ delta = @_getPos(e) - _startPos
75
+ pctDelta = (delta / total) * 100
76
+ @_applyResize(_dragging, _startSizes[_dragging] + pctDelta, _startSizes[_dragging + 1] - pctDelta)
77
+
78
+ _onPointerUp: (e) ->
79
+ return unless _dragging?
80
+ handle = @_handles()[_dragging]
81
+ handle?.removeAttribute 'data-dragging'
82
+ _dragging = null
83
+ @emit 'resize', sizes
84
+
85
+ _applyResize: (idx, newLeft, newRight) ->
86
+ newLeft = Math.max(@minSize, Math.min(@maxSize, newLeft))
87
+ newRight = Math.max(@minSize, Math.min(@maxSize, newRight))
88
+ combined = sizes[idx] + sizes[idx + 1]
89
+ newRight = combined - newLeft
90
+ return if newRight < @minSize or newRight > @maxSize
91
+ updated = [...sizes]
92
+ updated[idx] = newLeft
93
+ updated[idx + 1] = newRight
94
+ sizes = updated
95
+
96
+ _onKeydown: (handleIdx, e) ->
97
+ step = 10
98
+ horiz = @orientation is 'horizontal'
99
+ delta = switch e.key
100
+ when (if horiz then 'ArrowRight' else 'ArrowDown') then step
101
+ when (if horiz then 'ArrowLeft' else 'ArrowUp') then -step
102
+ else null
103
+ return unless delta?
104
+ e.preventDefault()
105
+ @_applyResize(handleIdx, sizes[handleIdx] + delta, sizes[handleIdx + 1] - delta)
106
+ @emit 'resize', sizes
107
+
108
+ ~>
109
+ return unless _ready
110
+ @_panels().forEach (el, idx) =>
111
+ pct = sizes[idx] or 0
112
+ el.style.setProperty '--panel-size', "#{pct}%"
113
+ el.style.flexBasis = "#{pct}%"
114
+ @_handles().forEach (handle, idx) =>
115
+ handle.setAttribute 'aria-valuenow', Math.round(sizes[idx] or 0)
116
+ handle.setAttribute 'aria-orientation', @orientation
117
+
118
+ render
119
+ div ref: "_root", $orientation: @orientation
120
+ @pointermove: @_onPointerMove
121
+ @pointerup: @_onPointerUp
122
+ style: "display:flex; flex-direction:#{if @orientation is 'horizontal' then 'row' else 'column'}"
123
+ slot
@@ -0,0 +1,22 @@
1
+ # Skeleton — accessible headless loading placeholder
2
+ #
3
+ # Placeholder element shown while content is loading.
4
+ # Exposes dimensions as CSS custom properties for styling.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Skeleton
9
+ # Skeleton width: "200px", height: "1em"
10
+ # Skeleton circle: true, width: "48px"
11
+
12
+ export Skeleton = component
13
+ @width := null
14
+ @height := null
15
+ @circle := false
16
+ @label := 'Loading'
17
+
18
+ render
19
+ div role: "status", aria-busy: "true", aria-label: @label
20
+ style: "--skeleton-width: #{@width or 'auto'}; --skeleton-height: #{@height or 'auto'}"
21
+ $circle: @circle?!
22
+ slot
@@ -0,0 +1,17 @@
1
+ # Spinner — accessible headless loading indicator
2
+ #
3
+ # Announces loading state to screen readers via role="status".
4
+ # Exposes size as a CSS custom property for styling.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Spinner
9
+ # Spinner label: "Saving...", size: "24px"
10
+
11
+ export Spinner = component
12
+ @label := 'Loading'
13
+ @size := null
14
+
15
+ render
16
+ div role: "status", aria-label: @label
17
+ style: if @size then "--spinner-size: #{@size}" else undefined
@@ -0,0 +1,27 @@
1
+ # Table — accessible headless semantic table wrapper
2
+ #
3
+ # Lightweight wrapper for HTML tables with optional caption and
4
+ # striped rows. For data-heavy tables with virtual scrolling, use Grid.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Table caption: "Team members", striped: true
9
+ # thead
10
+ # tr
11
+ # th "Name"
12
+ # th "Role"
13
+ # tbody
14
+ # tr
15
+ # td "Alice"
16
+ # td "Engineer"
17
+
18
+ export Table = component
19
+ @caption := ''
20
+ @striped := false
21
+
22
+ render
23
+ div $striped: @striped?!
24
+ table
25
+ if @caption
26
+ caption @caption
27
+ slot
@@ -0,0 +1,48 @@
1
+ # Textarea — accessible headless auto-resizing text area
2
+ #
3
+ # Tracks focus, validation, and disabled state via data attributes.
4
+ # Optional auto-resize adjusts height to fit content. Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # Textarea value <=> bio, placeholder: "Tell us about yourself"
8
+ # Textarea value <=> notes, autoResize: true, rows: 3
9
+
10
+ export Textarea = component
11
+ @value := ''
12
+ @placeholder := ''
13
+ @disabled := false
14
+ @required := false
15
+ @rows := 3
16
+ @autoResize := false
17
+
18
+ focused := false
19
+ touched := false
20
+
21
+ onInput: (e) ->
22
+ @value = e.target.value
23
+ @_resize(e.target) if @autoResize
24
+
25
+ onFocus: -> focused = true
26
+ onBlur: ->
27
+ focused = false
28
+ touched = true
29
+
30
+ _resize: (el) ->
31
+ el.style.height = 'auto'
32
+ el.style.height = "#{el.scrollHeight}px"
33
+
34
+ mounted: ->
35
+ @_resize(@_root) if @autoResize and @value
36
+
37
+ render
38
+ textarea ref: "_root", value: @value, placeholder: @placeholder, rows: @rows
39
+ disabled: @disabled
40
+ required: @required
41
+ aria-disabled: @disabled?!
42
+ aria-required: @required?!
43
+ $disabled: @disabled?!
44
+ $focused: focused?!
45
+ $touched: touched?!
46
+ @input: @onInput
47
+ @focusin: @onFocus
48
+ @focusout: @onBlur
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.13.71",
3
+ "version": "3.13.73",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
package/src/browser.js CHANGED
@@ -119,6 +119,29 @@ async function processRipScripts() {
119
119
  await ui.launch('', opts);
120
120
  }
121
121
  }
122
+
123
+ // Step 6: data-reload enables SSE hot-reload from dev server
124
+ if (runtimeTag?.hasAttribute('data-reload')) {
125
+ let ready = false;
126
+ const es = new EventSource('/watch');
127
+ es.addEventListener('connected', () => {
128
+ if (ready) location.reload();
129
+ ready = true;
130
+ });
131
+ es.addEventListener('reload', (e) => {
132
+ if (e.data === 'styles') {
133
+ const t = Date.now();
134
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
135
+ if (new URL(l.href).origin !== location.origin) return;
136
+ const url = new URL(l.href);
137
+ url.searchParams.set('_r', t);
138
+ l.href = url.toString();
139
+ });
140
+ } else {
141
+ location.reload();
142
+ }
143
+ });
144
+ }
122
145
  }
123
146
 
124
147
  export { processRipScripts };