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.
- package/README.md +6 -4
- package/bin/rip +167 -12
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +808 -0
- package/docs/RIP-DUCKDB.md +477 -0
- package/docs/RIP-INTRO.md +396 -0
- package/docs/RIP-LANG.md +59 -5
- package/docs/RIP-SCHEMA.md +191 -8
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +3627 -1470
- package/docs/dist/rip.min.js +671 -244
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +61 -0
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +0 -7
- package/docs/ui/index.css +66 -23
- package/docs/ui/index.html +6 -6
- package/package.json +9 -3
- package/rip-loader.js +64 -2
- package/src/AGENTS.md +63 -36
- package/src/browser.js +96 -14
- package/src/compiler.js +960 -143
- package/src/components.js +794 -88
- package/src/{types-emit.js → dts.js} +181 -71
- package/src/grammar/README.md +1 -1
- package/src/grammar/grammar.rip +111 -97
- package/src/lexer.js +132 -18
- package/src/parser.js +203 -205
- package/src/repl.js +74 -6
- package/src/schema/runtime-orm.js +168 -4
- package/src/schema/runtime-validate.js +146 -2
- package/src/schema/runtime.generated.js +314 -6
- package/src/schema/schema.js +5 -5
- package/src/sourcemaps.js +277 -1
- package/src/stdlib.js +253 -0
- package/src/typecheck.js +2023 -106
- package/src/types.js +127 -7
- package/docs/ui/accordion.rip +0 -103
- package/docs/ui/alert-dialog.rip +0 -53
- package/docs/ui/autocomplete.rip +0 -115
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -47
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -61
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -130
- package/docs/ui/context-menu.rip +0 -88
- package/docs/ui/date-picker.rip +0 -206
- package/docs/ui/dialog.rip +0 -60
- package/docs/ui/drawer.rip +0 -58
- package/docs/ui/editable-value.rip +0 -82
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -134
- package/docs/ui/menubar.rip +0 -151
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -203
- package/docs/ui/native-select.rip +0 -33
- package/docs/ui/nav-menu.rip +0 -126
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -93
- package/docs/ui/preview-card.rip +0 -75
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -57
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -151
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -113
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -71
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -38
- package/docs/ui/tooltip.rip +0 -85
- package/src/app.rip +0 -1571
- package/src/sourcemap-merge.js +0 -287
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
- /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/
|
|
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
|
-
|
|
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
|
|
475
|
-
if (tokens[j].data?.
|
|
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.
|
|
480
|
-
if (t.data?.
|
|
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
|
-
|
|
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
|
}
|
package/docs/ui/accordion.rip
DELETED
|
@@ -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
|
package/docs/ui/alert-dialog.rip
DELETED
|
@@ -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
|
package/docs/ui/autocomplete.rip
DELETED
|
@@ -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))}"
|
package/docs/ui/avatar.rip
DELETED
|
@@ -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
|
package/docs/ui/breadcrumb.rip
DELETED
|
@@ -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
|
package/docs/ui/button-group.rip
DELETED
|
@@ -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
|
package/docs/ui/button.rip
DELETED
|
@@ -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
|