rip-lang 3.13.119 → 3.13.121

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 (71) hide show
  1. package/README.md +11 -2
  2. package/docs/RIP-LANG.md +4 -0
  3. package/docs/dist/rip.js +257 -27
  4. package/docs/dist/rip.min.js +183 -183
  5. package/docs/dist/rip.min.js.br +0 -0
  6. package/docs/ui/accordion.rip +103 -0
  7. package/docs/ui/alert-dialog.rip +53 -0
  8. package/docs/ui/autocomplete.rip +115 -0
  9. package/docs/ui/avatar.rip +37 -0
  10. package/docs/ui/badge.rip +15 -0
  11. package/docs/ui/breadcrumb.rip +47 -0
  12. package/docs/ui/button-group.rip +26 -0
  13. package/docs/ui/button.rip +23 -0
  14. package/docs/ui/card.rip +25 -0
  15. package/docs/ui/carousel.rip +110 -0
  16. package/docs/ui/checkbox-group.rip +61 -0
  17. package/docs/ui/checkbox.rip +33 -0
  18. package/docs/ui/collapsible.rip +50 -0
  19. package/docs/ui/combobox.rip +130 -0
  20. package/docs/ui/context-menu.rip +88 -0
  21. package/docs/ui/date-picker.rip +206 -0
  22. package/docs/ui/dialog.rip +60 -0
  23. package/docs/ui/drawer.rip +58 -0
  24. package/docs/ui/editable-value.rip +82 -0
  25. package/docs/ui/field.rip +53 -0
  26. package/docs/ui/fieldset.rip +22 -0
  27. package/docs/ui/form.rip +39 -0
  28. package/docs/ui/grid.rip +901 -0
  29. package/docs/ui/hljs-rip.js +209 -0
  30. package/docs/ui/index.css +1797 -0
  31. package/docs/ui/index.html +2385 -0
  32. package/docs/ui/input-group.rip +28 -0
  33. package/docs/ui/input.rip +36 -0
  34. package/docs/ui/label.rip +16 -0
  35. package/docs/ui/menu.rip +134 -0
  36. package/docs/ui/menubar.rip +151 -0
  37. package/docs/ui/meter.rip +36 -0
  38. package/docs/ui/multi-select.rip +203 -0
  39. package/docs/ui/native-select.rip +33 -0
  40. package/docs/ui/nav-menu.rip +126 -0
  41. package/docs/ui/number-field.rip +162 -0
  42. package/docs/ui/otp-field.rip +89 -0
  43. package/docs/ui/pagination.rip +123 -0
  44. package/docs/ui/popover.rip +93 -0
  45. package/docs/ui/preview-card.rip +75 -0
  46. package/docs/ui/progress.rip +25 -0
  47. package/docs/ui/radio-group.rip +57 -0
  48. package/docs/ui/resizable.rip +123 -0
  49. package/docs/ui/scroll-area.rip +145 -0
  50. package/docs/ui/select.rip +151 -0
  51. package/docs/ui/separator.rip +17 -0
  52. package/docs/ui/skeleton.rip +22 -0
  53. package/docs/ui/slider.rip +165 -0
  54. package/docs/ui/spinner.rip +17 -0
  55. package/docs/ui/table.rip +27 -0
  56. package/docs/ui/tabs.rip +113 -0
  57. package/docs/ui/textarea.rip +48 -0
  58. package/docs/ui/toast.rip +87 -0
  59. package/docs/ui/toggle-group.rip +71 -0
  60. package/docs/ui/toggle.rip +24 -0
  61. package/docs/ui/toolbar.rip +38 -0
  62. package/docs/ui/tooltip.rip +85 -0
  63. package/package.json +1 -1
  64. package/src/compiler.js +24 -12
  65. package/src/components.js +43 -6
  66. package/src/grammar/grammar.rip +2 -2
  67. package/src/lexer.js +26 -0
  68. package/src/parser.js +2 -2
  69. package/src/sourcemap-utils.js +91 -0
  70. package/src/typecheck.js +33 -8
  71. package/src/ui.rip +118 -2
package/src/components.js CHANGED
@@ -489,12 +489,14 @@ export function installComponentSupport(CodeGenerator, Lexer) {
489
489
  }
490
490
  }
491
491
 
492
+ let atLineStart = tag === 'IDENTIFIER' && (prevTag === 'INDENT' || prevTag === 'TERMINATOR' || prevTag === 'RENDER');
493
+
492
494
  if (isClsxCallEnd) {
493
495
  isTemplateElement = true;
494
496
  } else if (tag === 'IDENTIFIER' && isTemplateTag(token[1]) && !isAfterControlFlow) {
495
497
  isTemplateElement = true;
496
498
  } else if (tag === 'IDENTIFIER' && !isAfterControlFlow) {
497
- isTemplateElement = startsWithTag(tokens, i);
499
+ isTemplateElement = atLineStart || startsWithTag(tokens, i);
498
500
  } else if (tag === 'PROPERTY' || tag === 'STRING' || tag === 'STRING_END' || tag === 'NUMBER' || tag === 'BOOL' || tag === 'CALL_END' || tag === ')' || tag === 'PRESENCE') {
499
501
  isTemplateElement = startsWithTag(tokens, i);
500
502
  }
@@ -513,7 +515,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
513
515
  }
514
516
  }
515
517
  }
516
- let isBareTag = isClsxCallEnd || (tag === 'IDENTIFIER' && isTemplateTag(token[1])) || isClassOrIdTail;
518
+ let isBareTag = isClsxCallEnd || (tag === 'IDENTIFIER' && (isTemplateTag(token[1]) || atLineStart)) || isClassOrIdTail;
517
519
 
518
520
  if (isBareTag) {
519
521
  let callStartToken = gen('CALL_START', '(', token);
@@ -804,6 +806,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
804
806
 
805
807
  const sl = [];
806
808
  sl.push('class {');
809
+ sl.push(' declare _root: Element | null;');
810
+ sl.push(' emit(name: string, detail?: any): void {}');
807
811
 
808
812
  // Constructor — typed props for public state/readonly (matches DTS)
809
813
  const propEntries = [];
@@ -892,6 +896,9 @@ export function installComponentSupport(CodeGenerator, Lexer) {
892
896
 
893
897
  // Pre-scan render block for @event: @method bindings to type method params
894
898
  const eventMethodTypes = new Map();
899
+ for (const [eventName, methodName] of autoEventHandlers) {
900
+ eventMethodTypes.set(methodName, eventName);
901
+ }
895
902
  if (renderBlock) {
896
903
  const scanEvents = (node) => {
897
904
  if (!Array.isArray(node)) return;
@@ -929,7 +936,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
929
936
  // Methods
930
937
  for (const { name, func } of methods) {
931
938
  if (Array.isArray(func) && (func[0] === '->' || func[0] === '=>')) {
932
- const [, params, methodBody] = func;
939
+ let [, params, methodBody] = func;
940
+ if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(methodBody)) params = ['it'];
933
941
  let paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
934
942
  // Inject event type on untyped first param when method is bound to an event
935
943
  const boundEvent = eventMethodTypes.get(name);
@@ -1063,7 +1071,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1063
1071
  constructions.push(` };`);
1064
1072
  }
1065
1073
  }
1066
- } else if (typeof head === 'string' && head !== 'object' && head !== 'switch' && TEMPLATE_TAGS.has(head.split(/[.#]/)[0])) {
1074
+ } else if (typeof head === 'string' && head !== 'object' && head !== 'switch' && (TEMPLATE_TAGS.has(head.split(/[.#]/)[0]) ||
1075
+ (/^[a-z]/.test(head) && node.length > 1 && Array.isArray(node[1]) && (node[1][0] === '->' || node[1][0] === '=>')))) {
1067
1076
  const tagName = head.split(/[.#]/)[0];
1068
1077
  const iProps = extractIntrinsicProps(node.slice(1));
1069
1078
  const tagLine = node.loc?.r;
@@ -1203,7 +1212,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1203
1212
  lines.push(' for (const key in this._rest) this._applyInheritedProp(this._inheritedEl, key, this._rest[key]);');
1204
1213
  lines.push(' }');
1205
1214
  lines.push(' _applyInheritedProp(el, key, value) {');
1206
- lines.push(' if (!el || key === \'key\' || key === \'ref\' || key.startsWith(\'__bind_\')) return;');
1215
+ lines.push(' if (!el || key === \'key\' || key === \'ref\' || key === \'children\' || key.startsWith(\'__bind_\')) return;');
1207
1216
  lines.push(' if (key[0] === \'@\') {');
1208
1217
  lines.push(' const event = key.slice(1).split(\'.\')[0];');
1209
1218
  lines.push(' this._restHandlers || (this._restHandlers = {});');
@@ -1251,7 +1260,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1251
1260
  // --- Methods ---
1252
1261
  for (const { name, func } of methods) {
1253
1262
  if (Array.isArray(func) && (func[0] === '->' || func[0] === '=>')) {
1254
- const [, params, methodBody] = func;
1263
+ let [, params, methodBody] = func;
1264
+ if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(methodBody)) params = ['it'];
1255
1265
  const paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
1256
1266
  const transformed = this.reactiveMembers ? this.transformComponentMembers(methodBody) : methodBody;
1257
1267
  const isAsync = this.containsAwait(methodBody);
@@ -1494,6 +1504,33 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1494
1504
  return slotVar;
1495
1505
  }
1496
1506
 
1507
+ // Switch: convert to if/else-if chain for conditional rendering
1508
+ if (headStr === 'switch') {
1509
+ const disc = rest[0];
1510
+ const whens = rest[1] || [];
1511
+ const defaultCase = rest[2] || null;
1512
+ let chain = defaultCase;
1513
+ for (let i = whens.length - 1; i >= 0; i--) {
1514
+ const [, tests, body] = whens[i];
1515
+ let cond;
1516
+ if (disc === null) {
1517
+ cond = tests.length === 1 ? tests[0]
1518
+ : tests.reduce((a, t) => a ? ['||', a, t] : t, null);
1519
+ } else {
1520
+ cond = tests.length === 1 ? ['==', disc, tests[0]]
1521
+ : tests.map(t => ['==', disc, t]).reduce((a, c) => a ? ['||', a, c] : c, null);
1522
+ }
1523
+ chain = ['if', cond, body, chain];
1524
+ }
1525
+ if (chain) {
1526
+ if (Array.isArray(chain) && chain[0] === 'if') return this.generateConditional(chain);
1527
+ return this.generateTemplateBlock(chain);
1528
+ }
1529
+ const cv = this.newElementVar('c');
1530
+ this._createLines.push(`${cv} = document.createComment('switch');`);
1531
+ return cv;
1532
+ }
1533
+
1497
1534
  // HTML tag (possibly with #id, e.g. div#content)
1498
1535
  if (headStr && this.isHtmlTag(headStr) && !meta(head, 'text')) {
1499
1536
  let [tagName, id] = headStr.split('#');
@@ -772,8 +772,8 @@ grammar =
772
772
  o 'IMPORT ImportNamespaceSpecifier FROM String' , '["import", 2, 4]'
773
773
  o 'IMPORT { } FROM String' , '["import", "{}", 5]'
774
774
  o 'IMPORT { ImportSpecifierList OptComma } FROM String' , '["import", 3, 7]'
775
- o 'IMPORT ImportDefaultSpecifier , ImportNamespaceSpecifier FROM String' , '["import", [2, 4], 6]'
776
- o 'IMPORT ImportDefaultSpecifier , { ImportSpecifierList OptComma } FROM String', '["import", [2, 5], 9]'
775
+ o 'IMPORT ImportDefaultSpecifier , ImportNamespaceSpecifier FROM String' , '["import", 2, 4, 6]'
776
+ o 'IMPORT ImportDefaultSpecifier , { ImportSpecifierList OptComma } FROM String', '["import", 2, 5, 9]'
777
777
  ]
778
778
 
779
779
  ImportSpecifierList: [
package/src/lexer.js CHANGED
@@ -1638,6 +1638,26 @@ export class Lexer {
1638
1638
  return forward(1);
1639
1639
  }
1640
1640
 
1641
+ // Dotted keys: collapse `IDENTIFIER . PROPERTY . PROPERTY` into a STRING
1642
+ if (tokens[i - 1]?.[0] === 'PROPERTY' && tokens[i - 2]?.[0] === '.') {
1643
+ let j = i - 2;
1644
+ while (j >= 2 && tokens[j]?.[0] === '.' && (tokens[j - 1]?.[0] === 'PROPERTY' || tokens[j - 1]?.[0] === 'IDENTIFIER')) {
1645
+ j -= 2;
1646
+ }
1647
+ j += 1;
1648
+ if (tokens[j]?.[0] === 'IDENTIFIER' || tokens[j]?.[0] === 'PROPERTY') {
1649
+ let parts = [];
1650
+ for (let k = j; k < i; k += 2) parts.push(tokens[k][1]);
1651
+ let str = gen('STRING', `"${parts.join('.')}"`, tokens[j]);
1652
+ str.pre = tokens[j].pre;
1653
+ str.spaced = tokens[j].spaced;
1654
+ str.newLine = tokens[j].newLine;
1655
+ str.loc = tokens[j].loc;
1656
+ tokens.splice(j, i - j, str);
1657
+ i = j + 1;
1658
+ }
1659
+ }
1660
+
1641
1661
  // Find the start of this key
1642
1662
  let s = EXPRESSION_END.has(this.tokens[i - 1]?.[0]) ? stack[stack.length - 1]?.[1] ?? i - 1 : i - 1;
1643
1663
  if (this.tokens[i - 2]?.[0] === '@') s = i - 2;
@@ -1825,6 +1845,12 @@ export class Lexer {
1825
1845
  if (!this.tokens[j]) return false;
1826
1846
  if (this.tokens[j]?.[0] === '@' && this.tokens[j + 2]?.[0] === ':') return true;
1827
1847
  if (this.tokens[j + 1]?.[0] === ':') return true;
1848
+ // Dotted keys: IDENTIFIER . PROPERTY ... :
1849
+ if ((this.tokens[j]?.[0] === 'IDENTIFIER' || this.tokens[j]?.[0] === 'PROPERTY') && this.tokens[j + 1]?.[0] === '.') {
1850
+ let k = j + 2;
1851
+ while (this.tokens[k]?.[0] === 'PROPERTY' && this.tokens[k + 1]?.[0] === '.') k += 2;
1852
+ if (this.tokens[k]?.[0] === 'PROPERTY' && this.tokens[k + 1]?.[0] === ':') return true;
1853
+ }
1828
1854
  if (EXPRESSION_START.has(this.tokens[j]?.[0])) {
1829
1855
  let end = null;
1830
1856
  this.detectEnd(j + 1,
package/src/parser.js CHANGED
@@ -189,8 +189,8 @@ const parserInstance = {
189
189
  case 327: case 330: return ["import", "{}", $[$0]];
190
190
  case 328: case 329: return ["import", $[$0-2], $[$0]];
191
191
  case 331: return ["import", $[$0-4], $[$0]];
192
- case 332: return ["import", [$[$0-4], $[$0-2]], $[$0]];
193
- case 333: return ["import", [$[$0-7], $[$0-4]], $[$0]];
192
+ case 332: return ["import", $[$0-4], $[$0-2], $[$0]];
193
+ case 333: return ["import", $[$0-7], $[$0-4], $[$0]];
194
194
  case 344: return ["*", $[$0]];
195
195
  case 345: return ["export", "{}"];
196
196
  case 346: return ["export", $[$0-2]];
@@ -1,6 +1,37 @@
1
1
  // Shared source-map position utilities used by both the CLI type-checker
2
2
  // (src/typecheck.js) and the VS Code language server (packages/vscode/src/lsp.js).
3
3
 
4
+ // When a switch expression is the implicit return of a function, the compiler
5
+ // wraps it in an IIFE: `return (() => { switch ... })()`. Non-exhaustive
6
+ // switches produce TS2322 on the IIFE return, which source-maps to the `switch`
7
+ // line. This helper detects that pattern and remaps the diagnostic to the
8
+ // function's return-type annotation (matching TS behaviour on the raw .ts).
9
+ // Returns { line, col, len } if remapped, or null if no adjustment needed.
10
+ export function adjustSwitchDiagnostic(source, pos, code) {
11
+ if (code !== 2322) return null;
12
+ const srcLines = source.split('\n');
13
+ const line = srcLines[pos.line] || '';
14
+ if (!/^\s*switch\b/.test(line)) return null;
15
+
16
+ const switchIndent = line.match(/^(\s*)/)[1].length;
17
+ for (let i = pos.line - 1; i >= 0; i--) {
18
+ const defLine = srcLines[i];
19
+ const defMatch = defLine.match(/^(\s*)def\b/);
20
+ if (defMatch && defMatch[1].length < switchIndent) {
21
+ // Found enclosing function — look for return-type annotation "):: Type"
22
+ const retMatch = defLine.match(/\)\s*::\s*(\w+)\s*$/);
23
+ if (retMatch) {
24
+ const typeStart = defLine.lastIndexOf(retMatch[1]);
25
+ return { line: i, col: typeStart, len: retMatch[1].length };
26
+ }
27
+ return { line: i, col: defMatch[1].length, len: 3 }; // fallback: highlight `def`
28
+ }
29
+ // Stop if we leave the indentation context
30
+ if (/\S/.test(defLine) && !defLine.match(/^(\s*)/)[1].length && !/^\s*#/.test(defLine)) break;
31
+ }
32
+ return null;
33
+ }
34
+
4
35
  export function getLineText(text, lineNum) {
5
36
  let start = 0, line = 0;
6
37
  for (let i = 0; i <= text.length; i++) {
@@ -73,6 +104,22 @@ export function mapToSourcePos(entry, offset) {
73
104
  const srcLines = entry.source.split('\n');
74
105
  const re = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
75
106
 
107
+ // For let/var declarations, the error word may appear on many source lines
108
+ // (e.g. `Status` referenced in multiple variable annotations). Narrow the
109
+ // search to the source line that declares the same variable.
110
+ const letMatch = genLineText.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var)\s+(\w+)/);
111
+ if (letMatch) {
112
+ const varName = letMatch[1];
113
+ const varRe = new RegExp('\\b' + varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*::');
114
+ for (let s = 0; s < srcLines.length; s++) {
115
+ if (varRe.test(srcLines[s])) {
116
+ const m = re.exec(srcLines[s]);
117
+ if (m) return { line: s, col: m.index };
118
+ return { line: s, col: 0 };
119
+ }
120
+ }
121
+ }
122
+
76
123
  // Find enclosing type/interface from DTS context to narrow search —
77
124
  // without this, duplicate member names (e.g. "host" in two types) always
78
125
  // resolve to the first occurrence in the source.
@@ -98,6 +145,50 @@ export function mapToSourcePos(entry, offset) {
98
145
  return null;
99
146
  }
100
147
 
148
+ // Hoisted multi-variable `let` declaration (e.g. `let a, b, items, ...;`) —
149
+ // the compiler aggregates variable declarations into one line with no useful
150
+ // per-variable source mapping. Detect the pattern (both top-level and inside
151
+ // functions), extract the word at the offset, and find its assignment in
152
+ // the Rip source. Use the genToSrc mapping of the preceding TS line (the
153
+ // function declaration) to scope the search and avoid matching a same-named
154
+ // variable in a different function.
155
+ const hoistLine = getLineText(entry.tsContent, tsLine);
156
+ if (/^\s*let\s+[$\w]+\s*,/.test(hoistLine) && entry.source) {
157
+ let hl = 0;
158
+ for (let i = 0; i < entry.tsContent.length; i++) {
159
+ if (hl === tsLine) { hl = i; break; }
160
+ if (entry.tsContent[i] === '\n') hl++;
161
+ }
162
+ const hCol = offset - hl;
163
+ const hWord = hoistLine.slice(hCol).match(/^[$\w]+/);
164
+ if (hWord) {
165
+ const word = hWord[0];
166
+ const srcLines = entry.source.split('\n');
167
+ const assignRe = new RegExp('^' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(?:::|=!|:=|~=|=)');
168
+
169
+ // Scope the search: find the source line of the enclosing function by
170
+ // checking genToSrc for the TS line just before the hoisted let.
171
+ let searchStart = 0;
172
+ if (entry.genToSrc) {
173
+ for (let g = tsLine - 1; g >= 0; g--) {
174
+ const s = entry.genToSrc.get(g);
175
+ if (s !== undefined) { searchStart = s; break; }
176
+ }
177
+ }
178
+
179
+ for (let s = searchStart; s < srcLines.length; s++) {
180
+ if (assignRe.test(srcLines[s].trimStart())) {
181
+ const col = srcLines[s].indexOf(word);
182
+ if (col >= 0) return { line: s, col };
183
+ }
184
+ }
185
+ // Variable is on a hoisted let but has no recognisable assignment in
186
+ // the source (e.g. for-loop iterators, destructured names). Return
187
+ // null so callers skip it rather than producing a garbage mapping.
188
+ return null;
189
+ }
190
+ }
191
+
101
192
  // Resolve source line from genToSrc
102
193
  let srcLine = entry.genToSrc.get(tsLine);
103
194
  if (srcLine === undefined) {
package/src/typecheck.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { Compiler, getStdlibCode } from './compiler.js';
14
14
  import { createRequire } from 'module';
15
15
  import { readFileSync, existsSync, readdirSync } from 'fs';
16
- import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol } from './sourcemap-utils.js';
16
+ import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic } from './sourcemap-utils.js';
17
17
  import { resolve, relative, dirname } from 'path';
18
18
  import { buildLineMap } from './sourcemaps.js';
19
19
 
@@ -22,7 +22,12 @@ import { buildLineMap } from './sourcemaps.js';
22
22
  // Detect type annotations (:: followed by space or =) ignoring comments,
23
23
  // string literals, and prototype syntax (Class::method).
24
24
  export function hasTypeAnnotations(source) {
25
+ let inHeredoc = false;
25
26
  return source.split('\n').some(line => {
27
+ // Track heredoc boundaries (''' or """)
28
+ const ticks = (line.match(/'''|"""/g) || []);
29
+ for (const t of ticks) inHeredoc = !inHeredoc;
30
+ if (inHeredoc) return false;
26
31
  // Strip comment
27
32
  line = line.replace(/#.*$/, '');
28
33
  // Strip string literals (single and double quoted)
@@ -247,6 +252,22 @@ export function cleanDiagnosticMessage(msg) {
247
252
  msg = msg.replace(/\b__ripEl\b/g, 'element');
248
253
  // Deduplicate consecutive identical lines (unwrapping can collapse nested messages)
249
254
  msg = msg.split('\n').filter((line, i, arr) => i === 0 || line.trim() !== arr[i - 1].trim()).join('\n');
255
+ // Remove redundant nested "Type 'X' is not assignable to type 'Y'" when
256
+ // the parent already says "Type 'X | Z' is not assignable to type 'Y'"
257
+ // and X is just one member of the union — the drill-down adds no information.
258
+ const lines = msg.split('\n');
259
+ if (lines.length >= 2) {
260
+ const parentMatch = lines[0].match(/^Type '(.+)' is not assignable to type '(.+)'\.$/);
261
+ if (parentMatch && parentMatch[1].includes(' | ')) {
262
+ const members = parentMatch[1].split(' | ').map(s => s.trim());
263
+ const filtered = lines.filter((line, i) => {
264
+ if (i === 0) return true;
265
+ const childMatch = line.trim().match(/^Type '(.+)' is not assignable to type '(.+)'\.$/);
266
+ return !(childMatch && childMatch[2] === parentMatch[2] && members.includes(childMatch[1]));
267
+ });
268
+ msg = filtered.join('\n');
269
+ }
270
+ }
250
271
  return msg;
251
272
  }
252
273
 
@@ -843,7 +864,7 @@ export function compileForCheck(filePath, source, compiler) {
843
864
 
844
865
  // ── Source mapping helpers (delegated to sourcemap-utils.js) ───────
845
866
 
846
- export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol } from './sourcemap-utils.js';
867
+ export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic } from './sourcemap-utils.js';
847
868
 
848
869
  // Map a TypeScript diagnostic offset back to a Rip source line number.
849
870
  // Returns -1 if the offset falls in the DTS header.
@@ -957,15 +978,15 @@ export async function runCheck(targetDir, opts = {}) {
957
978
  }
958
979
  }
959
980
 
960
- // Check for unresolved .rip imports in typed files
981
+ // Check for unresolved relative imports in all files (not just typed ones)
961
982
  const fileResults = [];
962
983
  let totalErrors = 0, totalWarnings = 0;
963
- for (const [fp, entry] of compiled) {
964
- if (!entry.hasTypes) continue;
965
- const srcLines = entry.source.split('\n');
984
+ for (const [fp, source] of sourcesByPath) {
985
+ const srcLines = source.split('\n');
966
986
  const errors = [];
967
987
  for (let s = 0; s < srcLines.length; s++) {
968
- const m = srcLines[s].match(/from\s+['"]([^'"]*\.rip)['"]/);
988
+ if (/^\s*#/.test(srcLines[s])) continue;
989
+ const m = srcLines[s].match(/^(?:import|export)\b.*from\s+['"](\.\.?\/[^'"]+)['"]/);
969
990
  if (!m) continue;
970
991
  const imported = resolve(dirname(fp), m[1]);
971
992
  if (!existsSync(imported)) {
@@ -1046,7 +1067,11 @@ export async function runCheck(targetDir, opts = {}) {
1046
1067
  const pos = mapToSourcePos(entry, d.start);
1047
1068
  if (!pos) continue;
1048
1069
 
1049
- const endPos = d.length ? mapToSourcePos(entry, d.start + d.length) : null;
1070
+ // Remap IIFE-switch diagnostics to the enclosing function declaration
1071
+ const adj = adjustSwitchDiagnostic(entry.source, pos, d.code);
1072
+ if (adj) { pos.line = adj.line; pos.col = adj.col; }
1073
+
1074
+ const endPos = adj ? { line: adj.line, col: adj.col + adj.len } : (d.length ? mapToSourcePos(entry, d.start + d.length) : null);
1050
1075
  const len = endPos && endPos.line === pos.line ? endPos.col - pos.col : 1;
1051
1076
 
1052
1077
  const message = cleanDiagnosticMessage(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
package/src/ui.rip CHANGED
@@ -1075,6 +1075,18 @@ _ariaPopupDismiss = (open, popup, close, els = [], repos = null) ->
1075
1075
  document.removeEventListener 'mousedown', onDown
1076
1076
  window.removeEventListener 'scroll', onScroll, true
1077
1077
 
1078
+ # popupGuard — per-component reopen suppression for pointer-driven popup closes.
1079
+ # Use when the same gesture that closes a popup can otherwise refocus/reclick a
1080
+ # trigger and reopen it immediately on the tail end of that sequence.
1081
+ _ariaPopupGuard = (delay = 250) ->
1082
+ blockedUntil = 0
1083
+ {
1084
+ block: (ms = delay) ->
1085
+ blockedUntil = Date.now() + ms
1086
+ canOpen: ->
1087
+ Date.now() >= blockedUntil
1088
+ }
1089
+
1078
1090
  # bindPopover — sync reactive open state with native Popover API
1079
1091
  # open: reactive boolean
1080
1092
  # popover: element or lazy getter (=> el)
@@ -1093,6 +1105,18 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
1093
1105
  return unless Object.hasOwn(HTMLElement.prototype, 'togglePopover')
1094
1106
  restoreEl = null
1095
1107
 
1108
+ syncState = (isOpen) ->
1109
+ if isOpen
1110
+ el.hidden = false
1111
+ try el.inert = false
1112
+ catch then null
1113
+ el.removeAttribute 'aria-hidden'
1114
+ else
1115
+ try el.inert = true
1116
+ catch then null
1117
+ el.setAttribute 'aria-hidden', 'true'
1118
+ el.hidden = true
1119
+
1096
1120
  restoreFocus = ->
1097
1121
  target = restoreEl
1098
1122
  restoreEl = null
@@ -1109,7 +1133,9 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
1109
1133
  isOpen = e.newState is 'open'
1110
1134
  if isOpen
1111
1135
  restoreEl = get(source) or currentFocus()
1136
+ syncState(true)
1112
1137
  else
1138
+ syncState(false)
1113
1139
  restoreFocus()
1114
1140
  setOpen?(isOpen)
1115
1141
  el.addEventListener 'toggle', onToggle
@@ -1120,6 +1146,7 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
1120
1146
  src = get(source)
1121
1147
  if desired
1122
1148
  restoreEl = src or currentFocus()
1149
+ syncState(true)
1123
1150
  opts = if src and desired then { force: desired, source: src } else { force: desired }
1124
1151
  try
1125
1152
  el.togglePopover(opts)
@@ -1127,6 +1154,8 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
1127
1154
  # If the element cannot be toggled right now (for example detached),
1128
1155
  # keep runtime stable; effect will retry on the next reactive pass.
1129
1156
  null
1157
+ else
1158
+ syncState(desired)
1130
1159
 
1131
1160
  -> el.removeEventListener 'toggle', onToggle
1132
1161
 
@@ -1147,6 +1176,18 @@ _ariaBindDialog = (open, dialog, setOpen, dismissable = true) ->
1147
1176
  return unless el?.showModal?
1148
1177
  restoreEl = null
1149
1178
 
1179
+ syncState = (isOpen) ->
1180
+ if isOpen
1181
+ el.hidden = false
1182
+ try el.inert = false
1183
+ catch then null
1184
+ el.removeAttribute 'aria-hidden'
1185
+ else
1186
+ try el.inert = true
1187
+ catch then null
1188
+ el.setAttribute 'aria-hidden', 'true'
1189
+ el.hidden = true
1190
+
1150
1191
  restoreFocus = ->
1151
1192
  target = restoreEl
1152
1193
  restoreEl = null
@@ -1167,16 +1208,20 @@ _ariaBindDialog = (open, dialog, setOpen, dismissable = true) ->
1167
1208
 
1168
1209
  onClose = ->
1169
1210
  setOpen?(false)
1211
+ syncState(false)
1170
1212
  restoreFocus()
1171
1213
  el.addEventListener 'cancel', onCancel
1172
1214
  el.addEventListener 'close', onClose
1173
1215
 
1174
1216
  if open and not el.open
1175
1217
  restoreEl = currentFocus() unless restoreEl
1218
+ syncState(true)
1176
1219
  try el.showModal()
1177
1220
  catch then null
1178
- if not open and el.open
1221
+ else if not open and el.open
1179
1222
  el.close()
1223
+ else
1224
+ syncState(!!open)
1180
1225
 
1181
1226
  ->
1182
1227
  el.removeEventListener 'cancel', onCancel
@@ -1262,10 +1307,81 @@ _ariaUnlockScroll = (instance) ->
1262
1307
  document.body.style.width = ''
1263
1308
  window.scrollTo 0, scrollY
1264
1309
 
1310
+ _ariaHasAnchor = do ->
1311
+ try
1312
+ return false unless document?.createElement
1313
+ anchor = document.createElement('div')
1314
+ floating = document.createElement('div')
1315
+ anchor.style.cssText = 'position:fixed;top:100px;left:100px;width:10px;height:10px;anchor-name:--probe'
1316
+ floating.style.cssText = 'position:fixed;inset:auto;margin:0;position-anchor:--probe;position-area:bottom start;width:10px;height:10px'
1317
+ document.body.appendChild(anchor)
1318
+ document.body.appendChild(floating)
1319
+ rect = floating.getBoundingClientRect()
1320
+ anchor.remove()
1321
+ floating.remove()
1322
+ rect.top > 50
1323
+ catch
1324
+ false
1325
+
1326
+ _ariaPosition = (trigger, floating, opts = {}) ->
1327
+ return unless trigger and floating
1328
+ placement = opts.placement ?? 'bottom start'
1329
+ offset = opts.offset ?? 4
1330
+ matchWidth = opts.matchWidth ?? false
1331
+ if _ariaHasAnchor
1332
+ name = "--anchor-#{floating.id or Math.random().toString(36).slice(2, 8)}"
1333
+ trigger.style.anchorName = name
1334
+ floating.style.positionAnchor = name
1335
+ floating.style.position = 'fixed'
1336
+ floating.style.inset = 'auto'
1337
+ floating.style.margin = '0'
1338
+ floating.style.positionArea = placement
1339
+ floating.style.positionTry = 'flip-block, flip-inline, flip-block flip-inline'
1340
+ floating.style.positionVisibility = 'anchors-visible'
1341
+ [side] = placement.split(' ')
1342
+ floating.style.marginTop = ''
1343
+ floating.style.marginBottom = ''
1344
+ floating.style.marginLeft = ''
1345
+ floating.style.marginRight = ''
1346
+ switch side
1347
+ when 'bottom' then floating.style.marginTop = "#{offset}px"
1348
+ when 'top' then floating.style.marginBottom = "#{offset}px"
1349
+ when 'left' then floating.style.marginRight = "#{offset}px"
1350
+ when 'right' then floating.style.marginLeft = "#{offset}px"
1351
+ floating.style.minWidth = 'anchor-size(width)' if matchWidth
1352
+ else
1353
+ rect = trigger.getBoundingClientRect()
1354
+ floating.style.position = 'fixed'
1355
+ floating.style.inset = 'auto'
1356
+ floating.style.margin = '0'
1357
+ [side, align] = placement.split(' ')
1358
+ align ??= 'start'
1359
+ switch side
1360
+ when 'bottom'
1361
+ floating.style.top = "#{rect.bottom + offset}px"
1362
+ when 'top'
1363
+ floating.style.bottom = "#{window.innerHeight - rect.top + offset}px"
1364
+ when 'left'
1365
+ floating.style.right = "#{window.innerWidth - rect.left + offset}px"
1366
+ when 'right'
1367
+ floating.style.left = "#{rect.right + offset}px"
1368
+ if side in ['bottom', 'top']
1369
+ switch align
1370
+ when 'start' then floating.style.left = "#{rect.left}px"
1371
+ when 'center' then floating.style.left = "#{rect.left + rect.width / 2}px"; floating.style.transform = 'translateX(-50%)'
1372
+ when 'end' then floating.style.right = "#{window.innerWidth - rect.right}px"
1373
+ else
1374
+ switch align
1375
+ when 'start' then floating.style.top = "#{rect.top}px"
1376
+ when 'center' then floating.style.top = "#{rect.top + rect.height / 2}px"; floating.style.transform = 'translateY(-50%)'
1377
+ when 'end' then floating.style.bottom = "#{window.innerHeight - rect.bottom}px"
1378
+ floating.style.minWidth = "#{rect.width}px" if matchWidth
1379
+
1265
1380
  globalThis.__aria ??= {
1266
- listNav: _ariaListNav, rovingNav: _ariaRovingNav, popupDismiss: _ariaPopupDismiss,
1381
+ listNav: _ariaListNav, rovingNav: _ariaRovingNav, popupDismiss: _ariaPopupDismiss, popupGuard: _ariaPopupGuard,
1267
1382
  bindPopover: _ariaBindPopover, bindDialog: _ariaBindDialog,
1268
1383
  positionBelow: _ariaPositionBelow, trapFocus: _ariaTrapFocus, wireAria: _ariaWireAria,
1269
1384
  lockScroll: _ariaLockScroll, unlockScroll: _ariaUnlockScroll,
1385
+ position: _ariaPosition, hasAnchor: _ariaHasAnchor,
1270
1386
  }
1271
1387
  globalThis.ARIA ??= globalThis.__aria