rip-lang 3.15.4 → 3.16.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 (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
package/src/types.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // `enum` declarations.
8
8
  //
9
9
  // The .d.ts emission half (emitTypes, intrinsic decl tables, component-
10
- // type emitter) lives in src/types-emit.js, which is reachable only from
10
+ // type emitter) lives in src/dts.js, which is reachable only from
11
11
  // CLI entry points and typecheck.js. Runtime enum codegen lives in
12
12
  // compiler.js (CodeEmitter.prototype.emitEnum) — that's real codegen,
13
13
  // not type machinery.
@@ -172,11 +172,19 @@ export function installTypeSupport(Lexer) {
172
172
  let afterEq = eqIdx + 1;
173
173
  let next = tokens[afterEq];
174
174
 
175
+ // Track identifiers in the type body so type-only imports get elided.
176
+ let trackBodyRefs = (start, end) => {
177
+ for (let k = start; k <= end && k < tokens.length; k++) {
178
+ if (tokens[k][0] === 'IDENTIFIER') typeRefNames.add(tokens[k][1]);
179
+ }
180
+ };
181
+
175
182
  // Block union: type Name = (TERMINATOR?) INDENT | "a" | "b" ... OUTDENT
176
183
  // Must check before structural — `=` suppresses TERMINATOR so INDENT follows directly
177
184
  if (next && (next[0] === 'TERMINATOR' || next[0] === 'INDENT')) {
178
185
  let result = collectBlockUnion(tokens, afterEq);
179
186
  if (result) {
187
+ trackBodyRefs(afterEq, result.endIdx);
180
188
  tokens.splice(removeFrom, result.endIdx - removeFrom + 1, makeDecl(result.typeText));
181
189
  return 0;
182
190
  }
@@ -185,12 +193,14 @@ export function installTypeSupport(Lexer) {
185
193
  // Structural type: type Name = INDENT ... OUTDENT
186
194
  if (next && next[0] === 'INDENT') {
187
195
  let endIdx = findMatchingOutdent(tokens, afterEq);
196
+ trackBodyRefs(afterEq, endIdx);
188
197
  tokens.splice(removeFrom, endIdx - removeFrom + 1, makeDecl(collectStructuralType(tokens, afterEq)));
189
198
  return 0;
190
199
  }
191
200
 
192
201
  // Simple alias: type Name = type-expression
193
202
  let typeTokens = collectTypeExpression(tokens, afterEq);
203
+ trackBodyRefs(afterEq, afterEq + typeTokens.consumed - 1);
194
204
  tokens.splice(removeFrom, afterEq + typeTokens.consumed - removeFrom, makeDecl(buildTypeString(typeTokens)));
195
205
  return 0;
196
206
  }
@@ -216,6 +226,10 @@ export function installTypeSupport(Lexer) {
216
226
  if (tokens[bodyIdx]?.[0] === 'INDENT') {
217
227
  let typeText = collectStructuralType(tokens, bodyIdx);
218
228
  let endIdx = findMatchingOutdent(tokens, bodyIdx);
229
+ for (let k = bodyIdx; k <= endIdx && k < tokens.length; k++) {
230
+ if (tokens[k][0] === 'IDENTIFIER') typeRefNames.add(tokens[k][1]);
231
+ }
232
+ if (extendsName) typeRefNames.add(extendsName);
219
233
  let declToken = gen('TYPE_DECL', name, nameToken);
220
234
  declToken.data = {
221
235
  name,
@@ -302,6 +316,7 @@ export function installTypeSupport(Lexer) {
302
316
  function collectTypeExpression(tokens, j) {
303
317
  let typeTokens = [];
304
318
  let depth = 0;
319
+ let bracketStack = []; // tracks innermost open bracket: '{', '[', '(', '<'
305
320
  let startJ = j;
306
321
 
307
322
  while (j < tokens.length) {
@@ -319,6 +334,8 @@ function collectTypeExpression(tokens, j) {
319
334
  // Handle >> as two > closes (nested generics: Map<string, Set<number>>)
320
335
  if (tTag === 'SHIFT' && t[1] === '>>' && depth >= 2) {
321
336
  depth -= 2;
337
+ if (bracketStack[bracketStack.length - 1] === '<') bracketStack.pop();
338
+ if (bracketStack[bracketStack.length - 1] === '<') bracketStack.pop();
322
339
  typeTokens.push(t);
323
340
  j++;
324
341
  continue;
@@ -326,6 +343,11 @@ function collectTypeExpression(tokens, j) {
326
343
 
327
344
  if (isOpen) {
328
345
  depth++;
346
+ let kind = (tTag === '{') ? '{'
347
+ : (tTag === '[' || tTag === 'INDEX_START') ? '['
348
+ : (tTag === 'COMPARE' && t[1] === '<') ? '<'
349
+ : '(';
350
+ bracketStack.push(kind);
329
351
  typeTokens.push(t);
330
352
  j++;
331
353
  continue;
@@ -333,6 +355,7 @@ function collectTypeExpression(tokens, j) {
333
355
  if (isClose) {
334
356
  if (depth > 0) {
335
357
  depth--;
358
+ bracketStack.pop();
336
359
  typeTokens.push(t);
337
360
  j++;
338
361
  continue;
@@ -362,6 +385,38 @@ function collectTypeExpression(tokens, j) {
362
385
  }
363
386
  }
364
387
 
388
+ // Inside a bracketed type expression, INDENT/OUTDENT/TERMINATOR are
389
+ // pure layout tokens (multi-line type literal `{ \n field: T \n }`).
390
+ // They carry no semantic meaning and would otherwise leak their raw
391
+ // `[1]` value (e.g. an indent level integer like `2`) into the
392
+ // type string. INDENT/OUTDENT are dropped silently; TERMINATOR
393
+ // separates fields and is replaced with a synthetic `;` so the
394
+ // emitted type literal is valid TS (`{ a: T; b: U }`).
395
+ if (depth > 0 && (tTag === 'INDENT' || tTag === 'OUTDENT')) {
396
+ j++;
397
+ continue;
398
+ }
399
+ if (depth > 0 && tTag === 'TERMINATOR') {
400
+ typeTokens.push(['', ';']);
401
+ j++;
402
+ continue;
403
+ }
404
+
405
+ // Inside `{ ... }` the Rip rewriter sometimes drops TERMINATOR
406
+ // between fields (e.g. after `Record<string, string[]>` because `>`
407
+ // looks like a binary operator wanting a RHS). Detect a new field
408
+ // by seeing a PROPERTY token at the top of a `{` and inject `;` if
409
+ // the previously emitted token isn't already a separator/opener.
410
+ if (tTag === 'PROPERTY' &&
411
+ bracketStack[bracketStack.length - 1] === '{') {
412
+ let prev = typeTokens[typeTokens.length - 1];
413
+ let prevTag = prev?.[0];
414
+ let prevVal = prev?.[1];
415
+ let needsSep = prev && prevTag !== '{' && prevTag !== ',' &&
416
+ !(prevTag === '' && prevVal === ';');
417
+ if (needsSep) typeTokens.push(['', ';']);
418
+ }
419
+
365
420
  // => at depth 0: function type arrow, continue collecting
366
421
  // -> at depth 0: code arrow, handled as delimiter above
367
422
  typeTokens.push(t);
@@ -378,7 +433,46 @@ function buildTypeString(typeTokens) {
378
433
  if (typeTokens.length === 0) return '';
379
434
  // Bare => (no params) means () => — add empty parens
380
435
  if (typeTokens[0]?.[0] === '=>') typeTokens.unshift(['', '()']);
381
- let typeStr = typeTokens.map(t => t[1]).join(' ').replace(/\s+/g, ' ').trim();
436
+ // Validation: `::` inside `{ ... }` in type position is illegal.
437
+ // `::` binds a name to a type (params, var decls, return types).
438
+ // Inside a structural type literal `{ ... }`, fields are key→type
439
+ // pairs and use `:` (TS-style), the same way TS type literals do.
440
+ // `::` has no role inside a type literal — every `:` there is
441
+ // already unambiguously a type separator.
442
+ {
443
+ let curlyDepth = 0;
444
+ for (let t of typeTokens) {
445
+ let tag = t[0];
446
+ if (tag === '{') curlyDepth++;
447
+ else if (tag === '}') curlyDepth--;
448
+ else if (tag === 'TYPE_ANNOTATION' && curlyDepth > 0) {
449
+ let loc = t.loc;
450
+ let where = loc ? ` (line ${loc.r}, col ${loc.c})` : '';
451
+ let err = new Error(
452
+ `Use \`:\` (not \`::\`) inside a structural type literal${where}. ` +
453
+ `\`::\` binds a name to a type; inside \`{ ... }\` in type ` +
454
+ `position, fields use \`:\` (TS-style).`
455
+ );
456
+ err.loc = loc;
457
+ throw err;
458
+ }
459
+ }
460
+ }
461
+ // Inline structural / function-param property-name optional marker:
462
+ // an IDENTIFIER carrying `.data.optional` and followed by TYPE_ANNOTATION
463
+ // gets a trailing `?` appended to its emitted name. The lexer stripped
464
+ // the trailing `?` from the token text but flagged it on `.data.optional`.
465
+ let parts = typeTokens.map((t, i) => {
466
+ let next = typeTokens[i + 1];
467
+ // Re-attach the trailing `?` for optional property names. The next
468
+ // separator is `::` (TYPE_ANNOTATION) in function param lists, or
469
+ // `:` inside an inline structural type literal `{ x?: T }`.
470
+ if (t.data?.optional && next && (next[0] === 'TYPE_ANNOTATION' || next[0] === ':')) {
471
+ return `${t[1]}?`;
472
+ }
473
+ return t[1];
474
+ });
475
+ let typeStr = parts.join(' ').replace(/\s+/g, ' ').trim();
382
476
  typeStr = typeStr
383
477
  .replace(/\s*<\s*/g, '<').replace(/\s*>\s*/g, '>')
384
478
  .replace(/\s*\[\s*/g, '[').replace(/\s*\]\s*/g, ']')
@@ -471,13 +565,13 @@ function collectStructuralType(tokens, indentIdx) {
471
565
  (/^[a-zA-Z_$]/.test(tokens[j][1]) && tokens[j + 1]?.[0] === 'TYPE_ANNOTATION'))) {
472
566
  readonly = true;
473
567
  propName = tokens[j][1];
474
- // Carry predicate flag through
475
- if (tokens[j].data?.predicate) optional = true;
568
+ // Carry optional flag through
569
+ if (tokens[j].data?.optional) optional = true;
476
570
  j++;
477
571
  }
478
572
 
479
- // Check for ? (optional property) — lexer stores as .data.predicate
480
- if (t.data?.predicate) optional = true;
573
+ // Check for ? (optional property) — lexer stores as .data.optional
574
+ if (t.data?.optional) optional = true;
481
575
  // Also check for standalone ? token
482
576
  if (tokens[j]?.[1] === '?' && !tokens[j]?.spaced) {
483
577
  optional = true;
@@ -519,7 +613,33 @@ function collectStructuralType(tokens, indentIdx) {
519
613
  let typeStr = buildTypeString(propTypeTokens);
520
614
  let prefix = readonly ? 'readonly ' : '';
521
615
  let optMark = optional ? '?' : '';
522
- props.push(`${prefix}${propName}${optMark}: ${typeStr}`);
616
+ // Method-shorthand: `name(args)` or `name(args): retType` — typeStr
617
+ // is parenthesized and has either nothing or `:` (not `::`) after the
618
+ // matching `)`. Emit without the property `:` separator so TS treats
619
+ // `this` inside the method as the containing type.
620
+ let methodShorthand = false;
621
+ if (!optional && typeStr.startsWith('(')) {
622
+ let depthM = 0;
623
+ for (let m = 0; m < typeStr.length; m++) {
624
+ let ch = typeStr[m];
625
+ if (ch === '(') depthM++;
626
+ else if (ch === ')') {
627
+ depthM--;
628
+ if (depthM === 0) {
629
+ let rest = typeStr.slice(m + 1).trimStart();
630
+ if (rest === '' || (rest.startsWith(':') && !rest.startsWith('::'))) {
631
+ methodShorthand = true;
632
+ }
633
+ break;
634
+ }
635
+ }
636
+ }
637
+ }
638
+ if (methodShorthand) {
639
+ props.push(`${prefix}${propName}${typeStr}`);
640
+ } else {
641
+ props.push(`${prefix}${propName}${optMark}: ${typeStr}`);
642
+ }
523
643
  } else {
524
644
  j++;
525
645
  }
@@ -1,103 +0,0 @@
1
- # Accordion — accessible headless expand/collapse widget
2
- #
3
- # Supports single or multiple expanded sections. Keyboard: Enter/Space to
4
- # toggle, ArrowDown/Up to move between triggers. Exposes $open on items.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Accordion multiple: false
9
- # div $item: "a"
10
- # button $trigger: true, "Section A"
11
- # div $content: true
12
- # p "Content A"
13
- # div $item: "b"
14
- # button $trigger: true, "Section B"
15
- # div $content: true
16
- # p "Content B"
17
-
18
- export Accordion = component
19
- @multiple:: boolean := false
20
-
21
- openItems := new Set()
22
- _ready := false
23
- _id =! "acc-#{Math.random().toString(36).slice(2, 8)}"
24
-
25
- mounted: ->
26
- _ready = true
27
- @_content?.querySelectorAll('[data-trigger]').forEach (trigger) =>
28
- item = trigger.closest('[data-item]')
29
- return unless item
30
- id = item.dataset.item
31
- trigger.addEventListener 'click', =>
32
- return if item.hasAttribute('data-disabled')
33
- @toggle(id)
34
- trigger.addEventListener 'keydown', (e) => @onTriggerKeydown(e, id)
35
-
36
- ~>
37
- return unless _ready
38
- @_content?.querySelectorAll('[data-item]').forEach (item) =>
39
- id = item.dataset.item
40
- isOpen = openItems.has(id)
41
- item.toggleAttribute 'data-open', isOpen
42
- trigger = item.querySelector('[data-trigger]')
43
- content = item.querySelector('[data-content]')
44
- triggerId = "#{_id}-trigger-#{id}"
45
- panelId = "#{_id}-panel-#{id}"
46
- if trigger
47
- isDisabled = item.hasAttribute('data-disabled')
48
- trigger.id = triggerId
49
- trigger.setAttribute 'aria-expanded', isOpen
50
- trigger.setAttribute 'aria-controls', panelId
51
- if isDisabled then trigger.setAttribute 'aria-disabled', true else trigger.removeAttribute 'aria-disabled'
52
- trigger.tabIndex = if isDisabled then -1 else 0
53
- if content
54
- content.id = panelId
55
- content.hidden = if isOpen then false else 'until-found'
56
- content.setAttribute 'role', 'region'
57
- content.setAttribute 'aria-labelledby', triggerId
58
- if isOpen
59
- rect = content.getBoundingClientRect()
60
- content.style.setProperty '--accordion-panel-height', "#{rect.height}px"
61
- content.style.setProperty '--accordion-panel-width', "#{rect.width}px"
62
-
63
- toggle: (id) ->
64
- if openItems.has(id)
65
- openItems.delete(id)
66
- else
67
- openItems.clear() unless @multiple
68
- openItems.add(id)
69
- openItems = new Set(openItems)
70
- @emit 'change', Array.from(openItems)
71
-
72
- isOpen: (id) ->
73
- openItems.has(id)
74
-
75
- onTriggerKeydown: (e, id) ->
76
- disabled = e.currentTarget.closest('[data-item]')?.hasAttribute('data-disabled')
77
- ARIA.rovingNav e, {
78
- next: => @_focusNext(1)
79
- prev: => @_focusNext(-1)
80
- first: => @_focusTrigger(0)
81
- last: => @_focusTrigger(-1)
82
- select: => @toggle(id) unless disabled
83
- }, 'vertical'
84
-
85
- _triggers: ->
86
- return [] unless @_content
87
- Array.from(@_content.querySelectorAll('[data-trigger]'))
88
-
89
- _focusNext: (dir) ->
90
- triggers = @_triggers()
91
- idx = triggers.indexOf(document.activeElement)
92
- return if idx is -1
93
- next = (idx + dir) %% triggers.length
94
- triggers[next]?.focus()
95
-
96
- _focusTrigger: (idx) ->
97
- triggers = @_triggers()
98
- target = if idx < 0 then triggers[triggers.length - 1] else triggers[idx]
99
- target?.focus()
100
-
101
- render
102
- div ref: "_content"
103
- slot
@@ -1,53 +0,0 @@
1
- # AlertDialog — accessible headless non-dismissable modal
2
- #
3
- # A Dialog variant that requires explicit user action to close.
4
- # Cannot be dismissed by clicking outside or pressing Escape.
5
- # Use for destructive confirmations, unsaved changes, etc.
6
- # Ships zero CSS.
7
- #
8
- # Usage:
9
- # AlertDialog open <=> showConfirm
10
- # h2 "Delete account?"
11
- # p "This action cannot be undone."
12
- # button @click: (=> showConfirm = false), "Cancel"
13
- # button @click: handleDelete, "Delete"
14
-
15
- export AlertDialog = component
16
- @open:: boolean := false
17
- @initialFocus:: any := null
18
-
19
- _prevFocus = null
20
- _id =! "adlg-#{Math.random().toString(36).slice(2, 8)}"
21
-
22
- ~>
23
- ARIA.bindDialog @open, (=> @_dialog), ((isOpen) =>
24
- if not isOpen and @open
25
- @open = false
26
- @emit 'close'
27
- ), false
28
-
29
- ~>
30
- if @open
31
- _prevFocus = document.activeElement
32
- ARIA.lockScroll(this)
33
- requestAnimationFrame =>
34
- panel = @_dialog
35
- if panel
36
- ARIA.wireAria panel, _id
37
- panel.setAttribute 'role', 'alertdialog'
38
- if @initialFocus
39
- target = if typeof @initialFocus is 'string' then panel.querySelector(@initialFocus) else @initialFocus
40
- target?.focus()
41
- else
42
- panel.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')?[0]?.focus()
43
- return ->
44
- ARIA.unlockScroll(this)
45
- _prevFocus?.focus()
46
-
47
- close: ->
48
- @open = false
49
- @emit 'close'
50
-
51
- render
52
- dialog ref: "_dialog", $open: @open?!
53
- slot
@@ -1,115 +0,0 @@
1
- # Autocomplete — accessible headless suggestion input
2
- #
3
- # Like Combobox but the input value IS the value (no selection from a list).
4
- # Suggestions are shown as the user types; selecting a suggestion fills the input.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Autocomplete value <=> city, items: cities, @filter: filterCities
9
-
10
- acCollator = new Intl.Collator(undefined, { sensitivity: 'base' })
11
-
12
- export Autocomplete = component
13
- @value:: string := ""
14
- @items:: any[] := []
15
- @placeholder:: string := "Type to search..."
16
- @disabled:: boolean := false
17
-
18
- open := false
19
-
20
- filteredItems ~=
21
- q = @value.trim()
22
- return @items unless q
23
- @items.filter (item) ->
24
- label = if typeof item is 'string' then item else (item.label or item.name or String(item))
25
- acCollator.compare(label.slice(0, q.length), q) is 0
26
-
27
- _listId =! "ac-list-#{Math.random().toString(36).slice(2, 8)}"
28
-
29
- _getItems: ->
30
- return [] unless @_list
31
- Array.from(@_list.querySelectorAll('[role="option"]'))
32
-
33
- _updateHighlight: ->
34
- idx = @_hlIdx
35
- opts = @_getItems()
36
- opts.forEach (el, ndx) ->
37
- el.id = "#{@_listId}-opt-#{ndx}" unless el.id
38
- el.toggleAttribute 'data-highlighted', ndx is idx
39
- activeId = if idx >= 0 and opts[idx] then opts[idx].id else undefined
40
- if @_input
41
- if activeId then @_input.setAttribute 'aria-activedescendant', activeId
42
- else @_input.removeAttribute 'aria-activedescendant'
43
- opts[idx]?.scrollIntoView({ block: 'nearest' })
44
-
45
- openMenu: ->
46
- open = true
47
- @_hlIdx = -1
48
- @_input?.focus()
49
-
50
- close: ->
51
- open = false
52
- @_hlIdx = -1
53
- @_input?.focus()
54
-
55
- _applyPlacement: ->
56
- ARIA.position @_input, @_list, placement: 'bottom start', offset: 2, matchWidth: true
57
-
58
- selectIndex: (idx) ->
59
- item = filteredItems[idx]
60
- return unless item
61
- label = if typeof item is 'string' then item else (item.label or item.name or String(item))
62
- @value = label
63
- @_input?.value = label
64
- @emit 'select', item
65
- @close()
66
-
67
- onInput: (e) ->
68
- newVal = e.target.value
69
- return if newVal is @value
70
- @value = newVal
71
- open = true
72
- @_hlIdx = if filteredItems.length > 0 then 0 else -1
73
- setTimeout (=> @_updateHighlight()), 0
74
-
75
- onKeydown: (e) ->
76
- len = filteredItems.length
77
- ARIA.listNav e,
78
- next: => @openMenu() unless open; if len then @_hlIdx = (@_hlIdx + 1) %% len; @_updateHighlight()
79
- prev: => @openMenu() unless open; if len then @_hlIdx = if @_hlIdx <= 0 then len - 1 else @_hlIdx - 1; @_updateHighlight()
80
- first: => if len then @_hlIdx = 0; @_updateHighlight()
81
- last: => if len then @_hlIdx = len - 1; @_updateHighlight()
82
- select: => @selectIndex(@_hlIdx) if @_hlIdx >= 0
83
- dismiss: => @close()
84
- tab: => @close()
85
-
86
- ~>
87
- if @_list
88
- @_list.setAttribute 'popover', 'auto'
89
- @_applyPlacement()
90
- ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_input)
91
-
92
- mounted: ->
93
- @_hlIdx = -1
94
- @_input.value = @value if @_input and @value
95
-
96
- render
97
- . $open: open?!
98
-
99
- input ref: "_input", role: "combobox", type: "text"
100
- autocomplete: "off"
101
- aria-expanded: !!open
102
- aria-haspopup: "listbox"
103
- aria-autocomplete: "list"
104
- aria-controls: open ? _listId : undefined
105
- $disabled: @disabled?!
106
- disabled: @disabled
107
- placeholder: @placeholder
108
- @input: @onInput
109
-
110
- div ref: "_list", id: _listId, role: "listbox", $open: open?!, style: "position:fixed;margin:0;inset:auto"
111
- for item, idx in filteredItems
112
- div role: "option", tabindex: "-1"
113
- @click: (=> @selectIndex(idx))
114
- @mouseenter: (=> @_hlIdx = idx; @_updateHighlight())
115
- "#{if typeof item is 'string' then item else (item.label or item.name or String(item))}"
@@ -1,37 +0,0 @@
1
- # Avatar — accessible headless avatar
2
- #
3
- # Shows an image, falls back to initials or a generic icon placeholder.
4
- # Ships zero CSS.
5
- #
6
- # Usage:
7
- # Avatar src: user.photoUrl, alt: user.name, fallback: "AC"
8
- # Avatar fallback: "JD"
9
- # Avatar
10
-
11
- export Avatar = component
12
- @src:: string := ""
13
- @alt:: string := ""
14
- @fallback:: string := ""
15
-
16
- imgError := false
17
-
18
- _onError: -> imgError = true
19
-
20
- _initials ~=
21
- return @fallback if @fallback
22
- return '' unless @alt
23
- parts = @alt.trim().split(/\s+/)
24
- chars = parts.map (p) -> p[0]?.toUpperCase() or ''
25
- chars.slice(0, 2).join('')
26
-
27
- render
28
- span role: "img", aria-label: @alt or 'Avatar'
29
- $status: if @src and not imgError then 'image' else if _initials then 'fallback' else 'placeholder'
30
- if @src and not imgError
31
- img src: @src, alt: @alt, @error: @_onError
32
- else if _initials
33
- span $initials: true
34
- _initials
35
- else
36
- span $placeholder: true
37
- "?"
package/docs/ui/badge.rip DELETED
@@ -1,15 +0,0 @@
1
- # Badge — accessible headless inline label
2
- #
3
- # Decorative label for status, counts, or categories.
4
- # Ships zero CSS.
5
- #
6
- # Usage:
7
- # Badge "New"
8
- # Badge variant: "outline", "Beta"
9
-
10
- export Badge = component
11
- @variant:: "solid" | "outline" | "subtle" := "solid"
12
-
13
- render
14
- span $variant: @variant
15
- slot
@@ -1,47 +0,0 @@
1
- # Breadcrumb — accessible headless navigation breadcrumb
2
- #
3
- # Renders a navigation trail with separator between items.
4
- # The last item is automatically marked as the current page.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Breadcrumb
9
- # a $item: true, href: "/", "Home"
10
- # a $item: true, href: "/products", "Products"
11
- # span $item: true, "Widget Pro"
12
- #
13
- # Breadcrumb separator: ">"
14
- # a $item: true, href: "/", "Home"
15
- # span $item: true, "Settings"
16
-
17
- export Breadcrumb = component
18
- @separator:: string := "/"
19
- @label:: string := "Breadcrumb"
20
-
21
- _ready := false
22
-
23
- mounted: -> _ready = true
24
-
25
- _items ~=
26
- return [] unless _ready
27
- return [] unless @_content
28
- Array.from(@_content.querySelectorAll('[data-item]') or [])
29
-
30
- ~>
31
- return unless _ready
32
- items = _items
33
- return unless items.length
34
- @_content?.style.setProperty '--breadcrumb-separator', JSON.stringify(@separator)
35
- items.forEach (el, idx) =>
36
- isLast = idx is items.length - 1
37
- if isLast
38
- el.setAttribute 'aria-current', 'page'
39
- el.toggleAttribute 'data-current', true
40
- else
41
- el.removeAttribute 'aria-current'
42
- el.removeAttribute 'data-current'
43
-
44
- render
45
- nav aria-label: @label
46
- ol ref: "_content"
47
- slot
@@ -1,26 +0,0 @@
1
- # ButtonGroup — accessible headless button group
2
- #
3
- # Groups related buttons with proper ARIA semantics.
4
- # Ships zero CSS.
5
- #
6
- # Usage:
7
- # ButtonGroup
8
- # Button "Cut"
9
- # Button "Copy"
10
- # Button "Paste"
11
- # ButtonGroup orientation: "vertical", label: "Text formatting"
12
- # Toggle pressed <=> isBold, "Bold"
13
- # Toggle pressed <=> isItalic, "Italic"
14
-
15
- export ButtonGroup = component
16
- @orientation:: "horizontal" | "vertical" := "horizontal"
17
- @disabled:: boolean := false
18
- @label:: string := ""
19
-
20
- render
21
- div role: "group"
22
- aria-label: @label?!
23
- aria-orientation: @orientation
24
- $orientation: @orientation
25
- $disabled: @disabled?!
26
- slot
@@ -1,23 +0,0 @@
1
- # Button — accessible headless button
2
- #
3
- # Handles disabled-but-focusable pattern and pressed state.
4
- # Ships zero CSS.
5
- #
6
- # Usage:
7
- # Button @click: handleClick
8
- # "Save"
9
- # Button disabled: true
10
- # "Unavailable"
11
-
12
- export Button = component
13
- @disabled:: boolean := false
14
-
15
- onClick: ->
16
- return if @disabled
17
- @emit 'press'
18
-
19
- render
20
- button disabled: @disabled
21
- aria-disabled: @disabled?!
22
- $disabled: @disabled?!
23
- slot
package/docs/ui/card.rip DELETED
@@ -1,25 +0,0 @@
1
- # Card — accessible headless content container
2
- #
3
- # Structured container with optional header, content, and footer sections.
4
- # Use $header, $content, $footer on children to mark sections.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Card
9
- # div $header: true
10
- # h3 "Title"
11
- # div $content: true
12
- # p "Body text"
13
- # div $footer: true
14
- # Button "Action"
15
- #
16
- # Card interactive: true, @click: handleClick
17
- # p "Clickable card"
18
-
19
- export Card = component
20
- @interactive:: boolean := false
21
-
22
- render
23
- div tabindex: (if @interactive then "0" else undefined)
24
- $interactive: @interactive?!
25
- slot