b44ui 0.0.3 → 0.0.4
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/{comps.tsx → index.tsx} +30 -23
- package/package.json +14 -5
- package/prompt.md +257 -0
- package/Markdown.tsx +0 -12
- package/cn.ts +0 -4
- package/index.ts +0 -3
package/{comps.tsx → index.tsx}
RENAMED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { type ReactNode, Children, useState } from "react"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { type ReactNode, Children, useMemo, useState } from "react"
|
|
2
|
+
import { Marked } from "marked"
|
|
3
|
+
import { markedHighlight } from "marked-highlight"
|
|
4
|
+
import hljs from "highlight.js"
|
|
5
|
+
import { ClassValue } from "clsx"
|
|
6
|
+
import clsx from "clsx"
|
|
7
|
+
import { twMerge } from "tailwind-merge"
|
|
8
|
+
|
|
9
|
+
// const tints = {
|
|
10
|
+
// blue: 'bg-blue-500/20 border-blue-500', orange: 'bg-orange-500/20 border-orange-500',
|
|
11
|
+
// red: 'bg-red-500/20 border-red-500', purple: 'bg-purple-500/20 border-purple-500',
|
|
12
|
+
// } as const
|
|
13
|
+
// const btnColors = { blue: 'bg-blue-600 hover:bg-blue-500', red: 'bg-red-600 hover:bg-red-500', green: 'bg-green-600 hover:bg-green-500' } as const
|
|
14
|
+
|
|
15
|
+
export type Color = 'red' | 'blue' | 'orange' | 'purple' | 'yellow'
|
|
16
|
+
const tintCn = (c: Color) => `bg-${c}-500/20 border-${c}-500`
|
|
17
|
+
const colorCn = (c: Color) => `bg-${c}-600 hover:bg-${c}-500`
|
|
18
|
+
|
|
19
|
+
export const CN = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
|
|
19
20
|
|
|
20
21
|
export type DProps = { children?: ReactNode, cn?: any, style?: React.CSSProperties, grow?: boolean }
|
|
21
22
|
export const D = ({ children, cn, style, grow }: DProps) =>
|
|
@@ -51,10 +52,10 @@ export const Card = ({ children, cn, grow }: DProps) =>
|
|
|
51
52
|
export const Col = ({ children, cn, grow }: DProps) =>
|
|
52
53
|
<D cn={["flex flex-col gap-3", cn]} grow={grow}>{children}</D>
|
|
53
54
|
|
|
54
|
-
export const Popover = ({ children, text, color, cn }: DProps & { text: ReactNode, color?:
|
|
55
|
+
export const Popover = ({ children, text, color, cn }: DProps & { text: ReactNode, color?: Color }) => {
|
|
55
56
|
const [open, setOpen] = useState(false)
|
|
56
57
|
return <span className="relative inline-block" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
|
|
57
|
-
<span className={CN("cursor-pointer", color &&
|
|
58
|
+
<span className={CN("cursor-pointer", color && tintCn(color), color && "border-b-2", cn)}>{children}</span>
|
|
58
59
|
{open && <div className="absolute left-0 top-full z-10 mt-1">
|
|
59
60
|
<Card cn="w-64 p-3 shadow-lg text-sm">{text}</Card>
|
|
60
61
|
</div>}
|
|
@@ -66,11 +67,11 @@ export const Muted = ({ children, cn, grow }: DProps) =>
|
|
|
66
67
|
|
|
67
68
|
export const Btn = ({ children, cn, grow, click, color, ghost, ...rest }: DProps & { click?: () => void, color?: Color, ghost?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) =>
|
|
68
69
|
<button className={CN("rounded px-4 py-2 text-sm cursor-pointer",
|
|
69
|
-
ghost ? "bg-transparent text-zinc-400 hover:bg-zinc-800" : color ?
|
|
70
|
+
ghost ? "bg-transparent text-zinc-400 hover:bg-zinc-800" : color ? colorCn(color) : "bg-zinc-800 hover:bg-zinc-700", grow && "flex-1", cn)}
|
|
70
71
|
onClick={click} {...rest}>{children}</button>
|
|
71
72
|
|
|
72
|
-
export const Tint = ({ children, color, cn, grow }: DProps & { color:
|
|
73
|
-
<D cn={["p-3 rounded text-sm",
|
|
73
|
+
export const Tint = ({ children, color, cn, grow }: DProps & { color: Color }) =>
|
|
74
|
+
<D cn={["p-3 rounded text-sm", tintCn(color), cn]} grow={grow}>{children}</D>
|
|
74
75
|
|
|
75
76
|
export const Row = ({ children, ratio, align, cn, grow }: DProps & { ratio?: number, align?: 'mid' | 'start' | 'end' }) =>
|
|
76
77
|
<div className={CN("flex gap-2 items-center", align === 'mid' && "justify-between", align == 'end' && "justify-end", align == 'start' && "justify-start", grow && "flex-1", cn)}
|
|
@@ -92,3 +93,9 @@ export const Select = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & Rea
|
|
|
92
93
|
|
|
93
94
|
export const Grid = ({ children, cols, cn, grow }: DProps & { cols?: number }) =>
|
|
94
95
|
<D cn={["grid gap-4", cn]} grow={grow} style={{ gridTemplateColumns: `repeat(${cols ?? Children.count(children)}, 1fr)` }}>{children}</D>
|
|
96
|
+
|
|
97
|
+
const marked = new Marked(markedHighlight({ langPrefix: "hljs language-", highlight: c => hljs.highlightAuto(c).value }))
|
|
98
|
+
export const Md = ({ children, className }: { children: string, className?: string }) => {
|
|
99
|
+
const html = useMemo(() => marked.parse(children) as string, [children])
|
|
100
|
+
return <div className={CN("ui-markdown", className)} dangerouslySetInnerHTML={{ __html: html }} />
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "b44ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"scripts": {
|
|
6
|
-
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "npm run dev --prefix example"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.tsx",
|
|
10
|
+
"./styles.css": "./styles.css"
|
|
11
|
+
},
|
|
7
12
|
"dependencies": {
|
|
8
13
|
"clsx": "^2.1.1",
|
|
9
14
|
"highlight.js": "^11.11.0",
|
|
@@ -11,6 +16,10 @@
|
|
|
11
16
|
"marked-highlight": "^2.2.0",
|
|
12
17
|
"tailwind-merge": "^3.0.0"
|
|
13
18
|
},
|
|
14
|
-
"peerDependencies": {
|
|
15
|
-
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^19.2.14"
|
|
24
|
+
}
|
|
16
25
|
}
|
package/prompt.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
**write a style guide for agents using the `b44ui` component library**
|
|
2
|
+
|
|
3
|
+
# b44ui/index.tsx
|
|
4
|
+
```tsx
|
|
5
|
+
import { type ReactNode, Children, useMemo, useState } from "react"
|
|
6
|
+
import { Marked } from "marked"
|
|
7
|
+
import { markedHighlight } from "marked-highlight"
|
|
8
|
+
import hljs from "highlight.js"
|
|
9
|
+
import { ClassValue } from "clsx"
|
|
10
|
+
import clsx from "clsx"
|
|
11
|
+
import { twMerge } from "tailwind-merge"
|
|
12
|
+
|
|
13
|
+
// const tints = {
|
|
14
|
+
// blue: 'bg-blue-500/20 border-blue-500', orange: 'bg-orange-500/20 border-orange-500',
|
|
15
|
+
// red: 'bg-red-500/20 border-red-500', purple: 'bg-purple-500/20 border-purple-500',
|
|
16
|
+
// } as const
|
|
17
|
+
// const btnColors = { blue: 'bg-blue-600 hover:bg-blue-500', red: 'bg-red-600 hover:bg-red-500', green: 'bg-green-600 hover:bg-green-500' } as const
|
|
18
|
+
|
|
19
|
+
export type Color = 'red' | 'blue' | 'orange' | 'purple' | 'yellow'
|
|
20
|
+
const tintCn = (c: Color) => `bg-${c}-500/20 border-${c}-500`
|
|
21
|
+
const colorCn = (c: Color) => `bg-${c}-600 hover:bg-${c}-500`
|
|
22
|
+
|
|
23
|
+
export const CN = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
|
|
24
|
+
|
|
25
|
+
export type DProps = { children?: ReactNode, cn?: any, style?: React.CSSProperties, grow?: boolean }
|
|
26
|
+
export const D = ({ children, cn, style, grow }: DProps) =>
|
|
27
|
+
<div className={CN(cn, grow && "flex-1")} style={style}>{children}</div>
|
|
28
|
+
|
|
29
|
+
export const App = ({ children, center = true, width, cn }: DProps & { center?: boolean, width?: number }) => {
|
|
30
|
+
const inner = Children.map(children, c => typeof c == 'string' ? <Md>{c.trim()}</Md> : c)
|
|
31
|
+
|
|
32
|
+
return <D cn={["min-h-screen w-full bg-[#111] text-[#eee] font-[Inter]", center && "flex justify-center items-center", cn]}>
|
|
33
|
+
{center ? <div className="max-w-full px-5 py-10 space-y-5 *:mx-auto" style={{ width }}>{inner}</div> : inner}
|
|
34
|
+
</D>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Centered = ({ children, cn, grow, width = 700 }: DProps & { width?: number }) =>
|
|
38
|
+
<D cn={["mx-auto max-w-full px-6", cn]} grow={grow} style={{ width }}>{children}</D>
|
|
39
|
+
|
|
40
|
+
export const Block = ({ label, children, row, dashed, cn, grow }: DProps & { label?: ReactNode, row?: boolean, dashed?: boolean }) =>
|
|
41
|
+
<D cn={["rounded p-4 flex flex-col items-center gap-2", dashed && "border border-dashed border-zinc-700", cn]} grow={grow}>
|
|
42
|
+
{label && <span className="opacity-75">{label}</span>}
|
|
43
|
+
<div className={CN(row ? "flex-row" : "flex-col", "flex items-center gap-2")}>{children}</div>
|
|
44
|
+
</D>
|
|
45
|
+
|
|
46
|
+
export const BlockSm = ({ children, dashed, cn, grow }: DProps & { dashed?: boolean }) =>
|
|
47
|
+
<D cn={["rounded px-3 py-2 text-sm", dashed && "border border-dashed border-zinc-700", cn]} grow={grow}>{children}</D>
|
|
48
|
+
|
|
49
|
+
export const Chip = ({ children, cn, click }: DProps & { click?: () => void }) =>
|
|
50
|
+
<span className={CN("bg-zinc-800 rounded px-2 py-0.5 text-xs", click && "cursor-pointer hover:bg-zinc-700", cn)}
|
|
51
|
+
onClick={click}>{children}</span>
|
|
52
|
+
|
|
53
|
+
export const Card = ({ children, cn, grow }: DProps) =>
|
|
54
|
+
<D cn={["rounded border border-zinc-700 bg-zinc-900 p-4 space-y-3", cn]} grow={grow}>{children}</D>
|
|
55
|
+
|
|
56
|
+
export const Col = ({ children, cn, grow }: DProps) =>
|
|
57
|
+
<D cn={["flex flex-col gap-3", cn]} grow={grow}>{children}</D>
|
|
58
|
+
|
|
59
|
+
export const Popover = ({ children, text, color, cn }: DProps & { text: ReactNode, color?: Color }) => {
|
|
60
|
+
const [open, setOpen] = useState(false)
|
|
61
|
+
return <span className="relative inline-block" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
|
|
62
|
+
<span className={CN("cursor-pointer", color && tintCn(color), color && "border-b-2", cn)}>{children}</span>
|
|
63
|
+
{open && <div className="absolute left-0 top-full z-10 mt-1">
|
|
64
|
+
<Card cn="w-64 p-3 shadow-lg text-sm">{text}</Card>
|
|
65
|
+
</div>}
|
|
66
|
+
</span>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Muted = ({ children, cn, grow }: DProps) =>
|
|
70
|
+
<span className={CN("text-sm text-zinc-400", grow && "flex-1", cn)}>{children}</span>
|
|
71
|
+
|
|
72
|
+
export const Btn = ({ children, cn, grow, click, color, ghost, ...rest }: DProps & { click?: () => void, color?: Color, ghost?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) =>
|
|
73
|
+
<button className={CN("rounded px-4 py-2 text-sm cursor-pointer",
|
|
74
|
+
ghost ? "bg-transparent text-zinc-400 hover:bg-zinc-800" : color ? colorCn(color) : "bg-zinc-800 hover:bg-zinc-700", grow && "flex-1", cn)}
|
|
75
|
+
onClick={click} {...rest}>{children}</button>
|
|
76
|
+
|
|
77
|
+
export const Tint = ({ children, color, cn, grow }: DProps & { color: Color }) =>
|
|
78
|
+
<D cn={["p-3 rounded text-sm", tintCn(color), cn]} grow={grow}>{children}</D>
|
|
79
|
+
|
|
80
|
+
export const Row = ({ children, ratio, align, cn, grow }: DProps & { ratio?: number, align?: 'mid' | 'start' | 'end' }) =>
|
|
81
|
+
<div className={CN("flex gap-2 items-center", align === 'mid' && "justify-between", align == 'end' && "justify-end", align == 'start' && "justify-start", grow && "flex-1", cn)}
|
|
82
|
+
style={ratio ? { width: `${ratio * 100}%` } : undefined}>{children}</div>
|
|
83
|
+
|
|
84
|
+
export const Modal = ({ children, open, cn }: DProps & { open: boolean }) =>
|
|
85
|
+
open ? <div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50">
|
|
86
|
+
<Card cn={["max-h-[80vh] overflow-y-auto w-lg", cn]}>{children}</Card>
|
|
87
|
+
</div> : null
|
|
88
|
+
|
|
89
|
+
export const Input = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & React.InputHTMLAttributes<HTMLInputElement>) =>
|
|
90
|
+
<input className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none", grow && "flex-1", cn)} {...rest} />
|
|
91
|
+
|
|
92
|
+
export const Textarea = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & React.TextareaHTMLAttributes<HTMLTextAreaElement>) =>
|
|
93
|
+
<textarea className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none w-full", grow && "flex-1", cn)} {...rest} />
|
|
94
|
+
|
|
95
|
+
export const Select = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & React.SelectHTMLAttributes<HTMLSelectElement>) =>
|
|
96
|
+
<select className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none cursor-pointer", grow && "flex-1", cn)} {...rest} />
|
|
97
|
+
|
|
98
|
+
export const Grid = ({ children, cols, cn, grow }: DProps & { cols?: number }) =>
|
|
99
|
+
<D cn={["grid gap-4", cn]} grow={grow} style={{ gridTemplateColumns: `repeat(${cols ?? Children.count(children)}, 1fr)` }}>{children}</D>
|
|
100
|
+
|
|
101
|
+
const marked = new Marked(markedHighlight({ langPrefix: "hljs language-", highlight: c => hljs.highlightAuto(c).value }))
|
|
102
|
+
export const Md = ({ children, className }: { children: string, className?: string }) => {
|
|
103
|
+
const html = useMemo(() => marked.parse(children) as string, [children])
|
|
104
|
+
return <div className={CN("ui-markdown", className)} dangerouslySetInnerHTML={{ __html: html }} />
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
# b44ui/example/app.tsx
|
|
109
|
+
```tsx
|
|
110
|
+
import { useState } from 'react'
|
|
111
|
+
import { App, Btn, Card, Chip, Col, D, Muted, Input, Row, Select, Grid } from 'ui'
|
|
112
|
+
|
|
113
|
+
type Meeting = { day: string, start: number, end: number }
|
|
114
|
+
type Section = { id: string, type: 'LEC' | 'TUT' | 'PRA', meetings: Meeting[] }
|
|
115
|
+
type Course = { code: string, name: string, sections: Section[] }
|
|
116
|
+
|
|
117
|
+
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
|
|
118
|
+
const hours = Array.from({ length: 13 }, (_, i) => i + 9) // 9am-9pm
|
|
119
|
+
|
|
120
|
+
const catalog: Course[] = [
|
|
121
|
+
{ code: 'CSC108', name: 'Intro to Computer Programming', sections: [
|
|
122
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Mon', start: 600, end: 660 }, { day: 'Wed', start: 600, end: 660 }] },
|
|
123
|
+
{ id: 'LEC0201', type: 'LEC', meetings: [{ day: 'Tue', start: 660, end: 720 }, { day: 'Thu', start: 660, end: 720 }] },
|
|
124
|
+
{ id: 'TUT0101', type: 'TUT', meetings: [{ day: 'Fri', start: 600, end: 660 }] },
|
|
125
|
+
{ id: 'TUT0201', type: 'TUT', meetings: [{ day: 'Fri', start: 660, end: 720 }] },
|
|
126
|
+
]},
|
|
127
|
+
{ code: 'MAT137', name: 'Calculus with Proofs', sections: [
|
|
128
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Mon', start: 540, end: 600 }, { day: 'Wed', start: 540, end: 600 }, { day: 'Fri', start: 540, end: 600 }] },
|
|
129
|
+
{ id: 'TUT0101', type: 'TUT', meetings: [{ day: 'Thu', start: 540, end: 600 }] },
|
|
130
|
+
{ id: 'TUT0201', type: 'TUT', meetings: [{ day: 'Thu', start: 600, end: 660 }] },
|
|
131
|
+
]},
|
|
132
|
+
{ code: 'CSC148', name: 'Intro to Computer Science', sections: [
|
|
133
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Tue', start: 540, end: 600 }, { day: 'Thu', start: 540, end: 600 }] },
|
|
134
|
+
{ id: 'PRA0101', type: 'PRA', meetings: [{ day: 'Wed', start: 720, end: 780 }] },
|
|
135
|
+
]},
|
|
136
|
+
{ code: 'PHY151', name: 'Foundations of Physics I', sections: [
|
|
137
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Mon', start: 720, end: 780 }, { day: 'Wed', start: 720, end: 780 }] },
|
|
138
|
+
{ id: 'TUT0101', type: 'TUT', meetings: [{ day: 'Fri', start: 720, end: 780 }] },
|
|
139
|
+
{ id: 'PRA0101', type: 'PRA', meetings: [{ day: 'Tue', start: 780, end: 840 }] },
|
|
140
|
+
]},
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
const colors = ['bg-blue-500', 'bg-emerald-500', 'bg-amber-500', 'bg-purple-500', 'bg-rose-500']
|
|
144
|
+
|
|
145
|
+
export default function YACS() {
|
|
146
|
+
const [query, setQuery] = useState('')
|
|
147
|
+
const [added, setAdded] = useState<Record<string, Course>>({})
|
|
148
|
+
const [picks, setPicks] = useState<Record<string, Record<string, number>>>({}) // code -> type -> section idx
|
|
149
|
+
|
|
150
|
+
const results = query.length ?
|
|
151
|
+
catalog.filter(c => !added[c.code] && (c.code.toLowerCase().includes(query.toLowerCase()) || c.name.toLowerCase().includes(query.toLowerCase())))
|
|
152
|
+
: []
|
|
153
|
+
|
|
154
|
+
const addCourse = (c: Course) => {
|
|
155
|
+
setAdded({ ...added, [c.code]: c })
|
|
156
|
+
const types = [...new Set(c.sections.map(s => s.type))]
|
|
157
|
+
setPicks({ ...picks, [c.code]: Object.fromEntries(types.map(t => [t, 0])) })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const removeCourse = (code: string) => {
|
|
161
|
+
const { [code]: _, ...rest } = added
|
|
162
|
+
setPicks(({ [code]: __, ...r }) => r)
|
|
163
|
+
setAdded(rest)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const cyclePick = (code: string, type: string) => {
|
|
167
|
+
const secs = added[code].sections.filter(s => s.type === type)
|
|
168
|
+
setPicks({ ...picks, [code]: { ...picks[code], [type]: (picks[code][type] + 1) % secs.length } })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// gather all visible meetings for calendar
|
|
172
|
+
const events: { code: string, meeting: Meeting, colorIdx: number }[] = []
|
|
173
|
+
const codes = Object.keys(added)
|
|
174
|
+
for (const [i, code] of codes.entries()) {
|
|
175
|
+
const c = added[code]
|
|
176
|
+
const p = picks[code] || {}
|
|
177
|
+
for (const type of Object.keys(p)) {
|
|
178
|
+
const secs = c.sections.filter(s => s.type === type)
|
|
179
|
+
const sec = secs[p[type]]
|
|
180
|
+
if (sec) for (const m of sec.meetings) events.push({ code, meeting: m, colorIdx: i })
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return <App center={false} cn="p-4 h-screen">
|
|
185
|
+
<Grid cols={3} cn="h-full">
|
|
186
|
+
|
|
187
|
+
{/* calendar */}
|
|
188
|
+
<Card cn="col-span-2 row-span-1 p-4 overflow-hidden flex flex-col">
|
|
189
|
+
<D cn="grid flex-1" style={{ gridTemplateColumns: `60px repeat(5, 1fr)` }}>
|
|
190
|
+
{/* hour labels */}
|
|
191
|
+
<D cn="flex flex-col justify-between text-sm text-zinc-500 pr-2">
|
|
192
|
+
{hours.map(h => <span key={h}>{h > 12 ? h - 12 : h}{h >= 12 ? 'pm' : 'am'}</span>)}
|
|
193
|
+
</D>
|
|
194
|
+
{/* day columns */}
|
|
195
|
+
{days.map(day => <D key={day} cn="relative border-l border-zinc-800">
|
|
196
|
+
<Muted cn="text-center block text-sm mb-1">{day}</Muted>
|
|
197
|
+
{/* hour gridlines */}
|
|
198
|
+
{hours.map(h => <D key={h} cn="border-t border-zinc-800/50 left-0 right-0 absolute" style={{ top: `${h*8}%` }} />)}
|
|
199
|
+
{/* events */}
|
|
200
|
+
{events.filter(e => e.meeting.day === day).map((e, i) => {
|
|
201
|
+
const top = ((e.meeting.start - 540) / (13 * 60)) * 100
|
|
202
|
+
const height = ((e.meeting.end - e.meeting.start) / (13 * 60)) * 100
|
|
203
|
+
return <D key={i} cn={`absolute left-0.5 right-0.5 rounded text-sm px-1 text-white ${colors[e.colorIdx % colors.length]}`}
|
|
204
|
+
style={{ top: `${top}%`, height: `${height}%` }}>
|
|
205
|
+
{e.code}
|
|
206
|
+
</D>
|
|
207
|
+
})}
|
|
208
|
+
</D>)}
|
|
209
|
+
</D>
|
|
210
|
+
</Card>
|
|
211
|
+
|
|
212
|
+
{/* right panel */}
|
|
213
|
+
<D cn="flex flex-col gap-3">
|
|
214
|
+
{/* search */}
|
|
215
|
+
<Card cn="p-4">
|
|
216
|
+
<Row>
|
|
217
|
+
<Input grow placeholder="search..." value={query} onChange={e => setQuery(e.target.value)} />
|
|
218
|
+
<Select><option>2026W</option><option>2026S</option></Select>
|
|
219
|
+
</Row>
|
|
220
|
+
<D cn="space-y-3 max-h-48 overflow-y-auto flex-row">
|
|
221
|
+
{results.map(c => <Btn key={c.code} click={() => addCourse(c)} grow>
|
|
222
|
+
{c.code} — {c.name}
|
|
223
|
+
</Btn>)}
|
|
224
|
+
{query.length >= 2 && !results.length && <Muted>no results</Muted>}
|
|
225
|
+
</D>
|
|
226
|
+
</Card>
|
|
227
|
+
|
|
228
|
+
{/* added */}
|
|
229
|
+
<Card cn="p-4 space-y-3 flex-1">
|
|
230
|
+
{codes.map((code, i) => {
|
|
231
|
+
const c = added[code]
|
|
232
|
+
const types = [...new Set(c.sections.map(s => s.type))]
|
|
233
|
+
return <D key={code} cn="space-y-1">
|
|
234
|
+
<Row align='mid'>
|
|
235
|
+
<span className={`w-2 h-2 rounded-full ${colors[i % colors.length]}`} />
|
|
236
|
+
<Muted cn="flex-1">{code}</Muted>
|
|
237
|
+
<Chip click={() => removeCourse(code)}>×</Chip>
|
|
238
|
+
</Row>
|
|
239
|
+
<Row>
|
|
240
|
+
{types.map(t => {
|
|
241
|
+
const secs = c.sections.filter(s => s.type === t)
|
|
242
|
+
const idx = picks[code]?.[t] ?? 0
|
|
243
|
+
return <Chip key={t} click={() => cyclePick(code, t)}>
|
|
244
|
+
{t} {secs[idx]?.id}
|
|
245
|
+
</Chip>
|
|
246
|
+
})}
|
|
247
|
+
</Row>
|
|
248
|
+
</D>
|
|
249
|
+
})}
|
|
250
|
+
{!codes.length && <Muted>no courses added</Muted>}
|
|
251
|
+
</Card>
|
|
252
|
+
</D>
|
|
253
|
+
|
|
254
|
+
</Grid>
|
|
255
|
+
</App>
|
|
256
|
+
}
|
|
257
|
+
```
|
package/Markdown.tsx
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react"
|
|
2
|
-
import { Marked } from "marked"
|
|
3
|
-
import { markedHighlight } from "marked-highlight"
|
|
4
|
-
import hljs from "highlight.js"
|
|
5
|
-
import cn from "./cn"
|
|
6
|
-
|
|
7
|
-
const marked = new Marked(markedHighlight({ langPrefix: "hljs language-", highlight: c => hljs.highlightAuto(c).value }))
|
|
8
|
-
|
|
9
|
-
export const Md = ({ children, className }: { children: string, className?: string }) => {
|
|
10
|
-
const html = useMemo(() => marked.parse(children) as string, [children])
|
|
11
|
-
return <div className={cn("ui-markdown", className)} dangerouslySetInnerHTML={{ __html: html }} />
|
|
12
|
-
}
|
package/cn.ts
DELETED
package/index.ts
DELETED