Skip to content

Commit 6087caf

Browse files
v0.0.12
1 parent 7fafd24 commit 6087caf

File tree

10 files changed

+428
-88
lines changed

10 files changed

+428
-88
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Version History
44

5+
### v0.0.12
6+
7+
- Started the development for the `<PointlessProcess⁄>` island.
8+
- Added an experimental implementation of the keynav utility.
9+
- Minor updates in the `<GibberishChat/>` and `<RandomQuote/>` islands.
10+
511
### v0.0.11
612

713
- Added a fix for the "Mixed content" error from the quotable API. Changed it to

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"@carcajada/teclas": "jsr:@carcajada/teclas@^1.0.8",
2323
"@egamagz/time-ago": "jsr:@egamagz/time-ago@^2025.4.9",
2424
"@lunchbox/ui": "jsr:@lunchbox/ui@3.0.0",
25+
"lunchbox-css": "npm:lunchbox-css@^0.1.6",
2526
"@vyn/cn": "jsr:@vyn/cn@^0.1.2",
26-
"lunchbox-css": "npm:lunchbox-css@^0.1.4",
2727
"@tailwindcss/typography": "npm:@tailwindcss/typography@^0.5.16",
2828
"daisyui": "npm:daisyui@^5.0.37",
2929
"fresh": "jsr:@fresh/core@^2.0.0-alpha.34",

islands/GibberishChat.tsx

Lines changed: 75 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,25 @@
11
import { cn } from "@vyn/cn";
22
import { useEffect, useState } from "preact/hooks";
3-
4-
interface Message {
5-
user: 0 | 1 | 2;
6-
content: string;
7-
}
8-
9-
function generateRandomWord(length: number): string {
10-
const alphabet = "abcdefghijklmnopqrstuvwxyz";
11-
let result = "";
12-
for (let i = 0; i < length; i++) {
13-
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
14-
}
15-
return result;
16-
}
17-
18-
function generateGibberish(words: number): string {
19-
const parts: string[] = [];
20-
for (let i = 0; i < words; i++) {
21-
const len = Math.floor(Math.random() * 7) + 2;
22-
parts.push(generateRandomWord(len));
23-
}
24-
return parts.join(" ");
25-
}
26-
27-
function transformSentence(str: string): string {
28-
if (str.length === 0) return "";
29-
return str.charAt(0).toUpperCase() + str.slice(1) + ".";
30-
}
31-
32-
const users = [
33-
generateRandomWord(2).toUpperCase(),
34-
generateRandomWord(2).toUpperCase(),
35-
"ME",
36-
];
3+
import {
4+
generateGibberish,
5+
generateRandomWord,
6+
transformSentence,
7+
} from "@/src/utils.ts";
378

389
export default function () {
39-
const [messages, setMessages] = useState<Message[]>([]);
10+
const { messages, loop, userSubmit, clear } = useGibberishChat();
4011

41-
useEffect(() => {
42-
const intervalId = setInterval(() => {
43-
const seed = Math.random();
44-
if (seed >= 0.1) return;
45-
46-
const simpleSeed = Math.trunc(seed * 100);
47-
48-
setMessages([{
49-
user: simpleSeed % 2 === 0 ? 0 : 1,
50-
content: transformSentence(generateGibberish((simpleSeed + 1) * 2)),
51-
}, ...messages]);
52-
}, 500);
53-
54-
return () => clearInterval(intervalId);
55-
}, [messages]);
12+
loop();
5613

5714
return (
5815
<div class="col-span-full md:col-span-3 lg:col-span-4">
59-
<div class="p-1-2 bg-base-200">
60-
Gibberish Chat
16+
<div class="prose p-1-2 bg-base-200">
17+
<h2>Gibberish Group Chat</h2>
6118
</div>
62-
<div class="p-1-2 dotted h-96 overflow-y-scroll flex flex-col-reverse">
19+
<div
20+
class="p-1-2 bg-dotted h-96 overflow-y-scroll flex flex-col-reverse"
21+
tabindex={0}
22+
>
6323
{messages.map((message) => (
6424
<div
6525
class={cn("chat", message.user === 2 ? "chat-end" : "chat-start")}
@@ -80,22 +40,8 @@ export default function () {
8040
</div>
8141
))}
8242
</div>
83-
<div class="p-1-2 bg-base-200">
84-
<form
85-
class="join"
86-
onSubmit={(ev) => {
87-
ev.preventDefault();
88-
const input = document
89-
.getElementById("gibberish-input") as HTMLInputElement;
90-
91-
setMessages([{
92-
user: 2,
93-
content: input.value,
94-
}, ...messages]);
95-
96-
input.value = "";
97-
}}
98-
>
43+
<div class="p-1-2 bg-base-200 flex justify-between">
44+
<form class="join" onSubmit={userSubmit}>
9945
<input
10046
id="gibberish-input"
10147
class="join-item input input-sm"
@@ -110,7 +56,67 @@ export default function () {
11056
Send
11157
</button>
11258
</form>
59+
<button
60+
tabIndex={0}
61+
class="btn btn-sm btn-soft"
62+
type="button"
63+
onClick={clear}
64+
>
65+
Clear messages
66+
</button>
11367
</div>
11468
</div>
11569
);
11670
}
71+
72+
interface Message {
73+
user: 0 | 1 | 2;
74+
content: string;
75+
}
76+
77+
const users = [
78+
generateRandomWord(2).toUpperCase(),
79+
generateRandomWord(2).toUpperCase(),
80+
"ME",
81+
];
82+
83+
function useGibberishChat() {
84+
const [messages, setMessages] = useState<Message[]>([]);
85+
86+
return {
87+
messages,
88+
89+
clear: () => setMessages([]),
90+
91+
loop: () =>
92+
useEffect(() => {
93+
const intervalId = setInterval(() => {
94+
if (messages.length > 20) return;
95+
const seed = Math.random();
96+
if (seed >= 0.1) return;
97+
98+
const simpleSeed = Math.trunc(seed * 100);
99+
100+
setMessages([{
101+
user: simpleSeed % 2 === 0 ? 0 : 1,
102+
content: transformSentence(generateGibberish((simpleSeed + 1) * 2)),
103+
}, ...messages]);
104+
}, 500);
105+
106+
return () => clearInterval(intervalId);
107+
}, [messages]),
108+
109+
userSubmit: (ev: Event) => {
110+
ev.preventDefault();
111+
const input = document
112+
.getElementById("gibberish-input") as HTMLInputElement;
113+
if (input.value && input.value.length > 0) {
114+
setMessages([{
115+
user: 2,
116+
content: input.value,
117+
}, ...messages]);
118+
}
119+
input.value = "";
120+
},
121+
};
122+
}

islands/Keynav.tsx

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,175 @@
1-
export { keynav as default } from "@lunchbox/ui";
1+
import { useEffect } from "preact/hooks";
2+
3+
const DIRECTIONS = [
4+
"ArrowUp",
5+
"ArrowDown",
6+
"ArrowLeft",
7+
"ArrowRight",
8+
];
9+
type Direction = typeof DIRECTIONS[number];
10+
11+
const SHAKE_CLASSES = [
12+
"shake_up",
13+
"shake_down",
14+
"shake_left",
15+
"shake_right",
16+
"shake",
17+
] as const;
18+
type ShakeClass = typeof SHAKE_CLASSES[number];
19+
20+
function overlaps(aMin: number, aMax: number, bMin: number, bMax: number) {
21+
return !(aMax < bMin || aMin > bMax);
22+
}
23+
24+
function findCandidate(
25+
current: HTMLElement,
26+
direction: Direction,
27+
candidates: HTMLElement[],
28+
padding: number = 0,
29+
): HTMLElement | null {
30+
const currentRect = current.getBoundingClientRect();
31+
const paddedY = {
32+
min: currentRect.top - padding,
33+
max: currentRect.bottom + padding,
34+
};
35+
const paddedX = {
36+
min: currentRect.left - padding,
37+
max: currentRect.right + padding,
38+
};
39+
40+
let closest: HTMLElement | null = null;
41+
let minDistance = Infinity;
42+
43+
for (const el of candidates) {
44+
if (el === current) continue;
45+
const rect = el.getBoundingClientRect();
46+
let distance: number = Infinity;
47+
48+
switch (direction) {
49+
case "ArrowRight":
50+
if (
51+
rect.left > currentRect.right &&
52+
overlaps(rect.top, rect.bottom, paddedY.min, paddedY.max)
53+
) {
54+
distance = rect.left - currentRect.right;
55+
} else {
56+
continue;
57+
}
58+
break;
59+
60+
case "ArrowLeft":
61+
if (
62+
rect.right < currentRect.left &&
63+
overlaps(rect.top, rect.bottom, paddedY.min, paddedY.max)
64+
) {
65+
distance = currentRect.left - rect.right;
66+
} else {
67+
continue;
68+
}
69+
break;
70+
71+
case "ArrowDown":
72+
if (
73+
rect.top > currentRect.bottom &&
74+
overlaps(rect.left, rect.right, paddedX.min, paddedX.max)
75+
) {
76+
distance = rect.top - currentRect.bottom;
77+
} else {
78+
continue;
79+
}
80+
break;
81+
82+
case "ArrowUp":
83+
if (
84+
rect.bottom < currentRect.top &&
85+
overlaps(rect.left, rect.right, paddedX.min, paddedX.max)
86+
) {
87+
distance = currentRect.top - rect.bottom;
88+
} else {
89+
continue;
90+
}
91+
break;
92+
}
93+
94+
if (distance < minDistance) {
95+
minDistance = distance;
96+
closest = el;
97+
}
98+
}
99+
100+
return closest;
101+
}
102+
103+
function resetShake(el: HTMLElement, exclude?: ShakeClass) {
104+
SHAKE_CLASSES.forEach((cls) => {
105+
if (cls !== exclude && el.classList.contains(cls)) {
106+
el.classList.remove(cls);
107+
}
108+
});
109+
}
110+
111+
function handleKeyDown(this: HTMLElement, e: KeyboardEvent) {
112+
const { key } = e;
113+
114+
if (key === "Enter") {
115+
resetShake(this);
116+
void this.offsetWidth;
117+
this.classList.add("shake");
118+
return;
119+
}
120+
121+
if (key === "Esc") this.blur();
122+
123+
if (!DIRECTIONS.includes(key)) return;
124+
125+
e.preventDefault();
126+
resetShake(this);
127+
128+
const tabbedElements = Array.from(
129+
document.querySelectorAll<HTMLElement>('[tabindex="0"]'),
130+
);
131+
const candidate = findCandidate(this, key, tabbedElements, 100);
132+
133+
if (candidate) {
134+
this.removeEventListener("keydown", handleKeyDown);
135+
candidate.focus();
136+
return;
137+
}
138+
139+
const dir = key.replace("Arrow", "").toLowerCase();
140+
const shakeClass = `shake_${dir}` as ShakeClass;
141+
142+
this.classList.add(shakeClass);
143+
}
144+
145+
/**
146+
* Attach the `handleKeyDown()` listener when an element is focused.
147+
*/
148+
function handleFocusIn(e: FocusEvent) {
149+
const t = e.target;
150+
if (t instanceof HTMLElement && t.tabIndex === 0) {
151+
t.addEventListener("keydown", handleKeyDown);
152+
}
153+
}
154+
155+
/**
156+
* Remove the `handleKeyDown()` listener when an element is focused.
157+
*/
158+
function handleFocusOut(e: FocusEvent) {
159+
const t = e.target;
160+
if (t instanceof HTMLElement && t.tabIndex === 0) {
161+
t.removeEventListener("keydown", handleKeyDown);
162+
resetShake(t);
163+
}
164+
}
165+
166+
function keynav() {
167+
document.addEventListener("focusin", handleFocusIn);
168+
document.addEventListener("focusout", handleFocusOut);
169+
}
170+
171+
export default function () {
172+
useEffect(keynav, []);
173+
174+
return null;
175+
}

0 commit comments

Comments
 (0)