b44ui 0.0.2 → 0.0.3
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 +76 -15
- package/example/src/App.tsx +144 -51
- package/package.json +1 -1
package/comps.tsx
CHANGED
|
@@ -1,33 +1,94 @@
|
|
|
1
|
-
import { type ReactNode, Children } from "react"
|
|
1
|
+
import { type ReactNode, Children, useState } from "react"
|
|
2
2
|
import CN from "./cn"
|
|
3
3
|
import { Md } from "./Markdown"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const tints = {
|
|
6
|
+
blue: 'bg-blue-500/20 border-blue-500',
|
|
7
|
+
orange: 'bg-orange-500/20 border-orange-500',
|
|
8
|
+
red: 'bg-red-500/20 border-red-500',
|
|
9
|
+
purple: 'bg-purple-500/20 border-purple-500',
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
const btnColors = {
|
|
13
|
+
blue: 'bg-blue-600 hover:bg-blue-500',
|
|
14
|
+
red: 'bg-red-600 hover:bg-red-500',
|
|
15
|
+
green: 'bg-green-600 hover:bg-green-500',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
export type Color = keyof typeof btnColors
|
|
19
|
+
|
|
20
|
+
export type DProps = { children?: ReactNode, cn?: any, style?: React.CSSProperties, grow?: boolean }
|
|
21
|
+
export const D = ({ children, cn, style, grow }: DProps) =>
|
|
22
|
+
<div className={CN(cn, grow && "flex-1")} style={style}>{children}</div>
|
|
8
23
|
|
|
9
24
|
export const App = ({ children, center = true, width, cn }: DProps & { center?: boolean, width?: number }) => {
|
|
10
25
|
const inner = Children.map(children, c => typeof c == 'string' ? <Md>{c.trim()}</Md> : c)
|
|
11
26
|
|
|
12
27
|
return <D cn={["min-h-screen w-full bg-[#111] text-[#eee] font-[Inter]", center && "flex justify-center items-center", cn]}>
|
|
13
|
-
{center ? <div className="max-w-full px-
|
|
28
|
+
{center ? <div className="max-w-full px-5 py-10 space-y-5 *:mx-auto" style={{ width }}>{inner}</div> : inner}
|
|
14
29
|
</D>
|
|
15
30
|
}
|
|
16
31
|
|
|
17
|
-
export const Centered = ({ children, cn, width = 700 }: DProps & { width?: number }) =>
|
|
18
|
-
<D cn={["mx-auto max-w-full px-6", cn]} style={{ width }}>{children}</D>
|
|
32
|
+
export const Centered = ({ children, cn, grow, width = 700 }: DProps & { width?: number }) =>
|
|
33
|
+
<D cn={["mx-auto max-w-full px-6", cn]} grow={grow} style={{ width }}>{children}</D>
|
|
19
34
|
|
|
20
|
-
export const Block = ({ label, children, row, dashed, cn }: DProps & { label?: ReactNode, row?: boolean, dashed?: boolean }) =>
|
|
21
|
-
<D cn={["rounded p-4 flex flex-col items-center gap-2", dashed && "border border-dashed border-zinc-700", cn]}>
|
|
35
|
+
export const Block = ({ label, children, row, dashed, cn, grow }: DProps & { label?: ReactNode, row?: boolean, dashed?: boolean }) =>
|
|
36
|
+
<D cn={["rounded p-4 flex flex-col items-center gap-2", dashed && "border border-dashed border-zinc-700", cn]} grow={grow}>
|
|
22
37
|
{label && <span className="opacity-75">{label}</span>}
|
|
23
38
|
<div className={CN(row ? "flex-row" : "flex-col", "flex items-center gap-2")}>{children}</div>
|
|
24
39
|
</D>
|
|
25
40
|
|
|
26
|
-
export const BlockSm = ({ children, dashed, cn }: DProps & { dashed?: boolean }) =>
|
|
27
|
-
<D cn={["rounded px-3 py-2 text-sm", dashed && "border border-dashed border-zinc-700", cn]}>{children}</D>
|
|
41
|
+
export const BlockSm = ({ children, dashed, cn, grow }: DProps & { dashed?: boolean }) =>
|
|
42
|
+
<D cn={["rounded px-3 py-2 text-sm", dashed && "border border-dashed border-zinc-700", cn]} grow={grow}>{children}</D>
|
|
43
|
+
|
|
44
|
+
export const Chip = ({ children, cn, click }: DProps & { click?: () => void }) =>
|
|
45
|
+
<span className={CN("bg-zinc-800 rounded px-2 py-0.5 text-xs", click && "cursor-pointer hover:bg-zinc-700", cn)}
|
|
46
|
+
onClick={click}>{children}</span>
|
|
47
|
+
|
|
48
|
+
export const Card = ({ children, cn, grow }: DProps) =>
|
|
49
|
+
<D cn={["rounded border border-zinc-700 bg-zinc-900 p-4 space-y-3", cn]} grow={grow}>{children}</D>
|
|
50
|
+
|
|
51
|
+
export const Col = ({ children, cn, grow }: DProps) =>
|
|
52
|
+
<D cn={["flex flex-col gap-3", cn]} grow={grow}>{children}</D>
|
|
53
|
+
|
|
54
|
+
export const Popover = ({ children, text, color, cn }: DProps & { text: ReactNode, color?: keyof typeof tints }) => {
|
|
55
|
+
const [open, setOpen] = useState(false)
|
|
56
|
+
return <span className="relative inline-block" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
|
|
57
|
+
<span className={CN("cursor-pointer", color && tints[color], color && "border-b-2", cn)}>{children}</span>
|
|
58
|
+
{open && <div className="absolute left-0 top-full z-10 mt-1">
|
|
59
|
+
<Card cn="w-64 p-3 shadow-lg text-sm">{text}</Card>
|
|
60
|
+
</div>}
|
|
61
|
+
</span>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const Muted = ({ children, cn, grow }: DProps) =>
|
|
65
|
+
<span className={CN("text-sm text-zinc-400", grow && "flex-1", cn)}>{children}</span>
|
|
66
|
+
|
|
67
|
+
export const Btn = ({ children, cn, grow, click, color, ghost, ...rest }: DProps & { click?: () => void, color?: Color, ghost?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) =>
|
|
68
|
+
<button className={CN("rounded px-4 py-2 text-sm cursor-pointer",
|
|
69
|
+
ghost ? "bg-transparent text-zinc-400 hover:bg-zinc-800" : color ? btnColors[color] : "bg-zinc-800 hover:bg-zinc-700", grow && "flex-1", cn)}
|
|
70
|
+
onClick={click} {...rest}>{children}</button>
|
|
71
|
+
|
|
72
|
+
export const Tint = ({ children, color, cn, grow }: DProps & { color: keyof typeof tints }) =>
|
|
73
|
+
<D cn={["p-3 rounded text-sm", tints[color], cn]} grow={grow}>{children}</D>
|
|
74
|
+
|
|
75
|
+
export const Row = ({ children, ratio, align, cn, grow }: DProps & { ratio?: number, align?: 'mid' | 'start' | 'end' }) =>
|
|
76
|
+
<div className={CN("flex gap-2 items-center", align === 'mid' && "justify-between", align == 'end' && "justify-end", align == 'start' && "justify-start", grow && "flex-1", cn)}
|
|
77
|
+
style={ratio ? { width: `${ratio * 100}%` } : undefined}>{children}</div>
|
|
78
|
+
|
|
79
|
+
export const Modal = ({ children, open, cn }: DProps & { open: boolean }) =>
|
|
80
|
+
open ? <div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50">
|
|
81
|
+
<Card cn={["max-h-[80vh] overflow-y-auto w-lg", cn]}>{children}</Card>
|
|
82
|
+
</div> : null
|
|
83
|
+
|
|
84
|
+
export const Input = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & React.InputHTMLAttributes<HTMLInputElement>) =>
|
|
85
|
+
<input className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none", grow && "flex-1", cn)} {...rest} />
|
|
86
|
+
|
|
87
|
+
export const Textarea = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & React.TextareaHTMLAttributes<HTMLTextAreaElement>) =>
|
|
88
|
+
<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} />
|
|
28
89
|
|
|
29
|
-
export const
|
|
30
|
-
<
|
|
90
|
+
export const Select = ({ cn, grow, ...rest }: { cn?: any, grow?: boolean } & React.SelectHTMLAttributes<HTMLSelectElement>) =>
|
|
91
|
+
<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} />
|
|
31
92
|
|
|
32
|
-
export const Grid = ({ children, cols, cn }:
|
|
33
|
-
<D cn={["grid gap-4", cn]} style={{ gridTemplateColumns: `repeat(${cols ?? Children.count(children)}, 1fr)` }}>{children}</D>
|
|
93
|
+
export const Grid = ({ children, cols, cn, grow }: DProps & { cols?: number }) =>
|
|
94
|
+
<D cn={["grid gap-4", cn]} grow={grow} style={{ gridTemplateColumns: `repeat(${cols ?? Children.count(children)}, 1fr)` }}>{children}</D>
|
package/example/src/App.tsx
CHANGED
|
@@ -1,54 +1,147 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { App, Btn, Card, Chip, Col, D, Muted, Input, Row, Select, Grid } from 'ui'
|
|
3
|
+
|
|
4
|
+
type Meeting = { day: string, start: number, end: number }
|
|
5
|
+
type Section = { id: string, type: 'LEC' | 'TUT' | 'PRA', meetings: Meeting[] }
|
|
6
|
+
type Course = { code: string, name: string, sections: Section[] }
|
|
7
|
+
|
|
8
|
+
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
|
|
9
|
+
const hours = Array.from({ length: 13 }, (_, i) => i + 9) // 9am-9pm
|
|
10
|
+
|
|
11
|
+
const catalog: Course[] = [
|
|
12
|
+
{ code: 'CSC108', name: 'Intro to Computer Programming', sections: [
|
|
13
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Mon', start: 600, end: 660 }, { day: 'Wed', start: 600, end: 660 }] },
|
|
14
|
+
{ id: 'LEC0201', type: 'LEC', meetings: [{ day: 'Tue', start: 660, end: 720 }, { day: 'Thu', start: 660, end: 720 }] },
|
|
15
|
+
{ id: 'TUT0101', type: 'TUT', meetings: [{ day: 'Fri', start: 600, end: 660 }] },
|
|
16
|
+
{ id: 'TUT0201', type: 'TUT', meetings: [{ day: 'Fri', start: 660, end: 720 }] },
|
|
17
|
+
]},
|
|
18
|
+
{ code: 'MAT137', name: 'Calculus with Proofs', sections: [
|
|
19
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Mon', start: 540, end: 600 }, { day: 'Wed', start: 540, end: 600 }, { day: 'Fri', start: 540, end: 600 }] },
|
|
20
|
+
{ id: 'TUT0101', type: 'TUT', meetings: [{ day: 'Thu', start: 540, end: 600 }] },
|
|
21
|
+
{ id: 'TUT0201', type: 'TUT', meetings: [{ day: 'Thu', start: 600, end: 660 }] },
|
|
22
|
+
]},
|
|
23
|
+
{ code: 'CSC148', name: 'Intro to Computer Science', sections: [
|
|
24
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Tue', start: 540, end: 600 }, { day: 'Thu', start: 540, end: 600 }] },
|
|
25
|
+
{ id: 'PRA0101', type: 'PRA', meetings: [{ day: 'Wed', start: 720, end: 780 }] },
|
|
26
|
+
]},
|
|
27
|
+
{ code: 'PHY151', name: 'Foundations of Physics I', sections: [
|
|
28
|
+
{ id: 'LEC0101', type: 'LEC', meetings: [{ day: 'Mon', start: 720, end: 780 }, { day: 'Wed', start: 720, end: 780 }] },
|
|
29
|
+
{ id: 'TUT0101', type: 'TUT', meetings: [{ day: 'Fri', start: 720, end: 780 }] },
|
|
30
|
+
{ id: 'PRA0101', type: 'PRA', meetings: [{ day: 'Tue', start: 780, end: 840 }] },
|
|
31
|
+
]},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const colors = ['bg-blue-500', 'bg-emerald-500', 'bg-amber-500', 'bg-purple-500', 'bg-rose-500']
|
|
35
|
+
|
|
36
|
+
export default function YACS() {
|
|
37
|
+
const [query, setQuery] = useState('')
|
|
38
|
+
const [added, setAdded] = useState<Record<string, Course>>({})
|
|
39
|
+
const [picks, setPicks] = useState<Record<string, Record<string, number>>>({}) // code -> type -> section idx
|
|
40
|
+
|
|
41
|
+
const results = query.length ?
|
|
42
|
+
catalog.filter(c => !added[c.code] && (c.code.toLowerCase().includes(query.toLowerCase()) || c.name.toLowerCase().includes(query.toLowerCase())))
|
|
43
|
+
: []
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
const addCourse = (c: Course) => {
|
|
46
|
+
setAdded({ ...added, [c.code]: c })
|
|
47
|
+
const types = [...new Set(c.sections.map(s => s.type))]
|
|
48
|
+
setPicks({ ...picks, [c.code]: Object.fromEntries(types.map(t => [t, 0])) })
|
|
49
|
+
}
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
const removeCourse = (code: string) => {
|
|
52
|
+
const { [code]: _, ...rest } = added
|
|
53
|
+
setPicks(({ [code]: __, ...r }) => r)
|
|
54
|
+
setAdded(rest)
|
|
55
|
+
}
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const cyclePick = (code: string, type: string) => {
|
|
58
|
+
const secs = added[code].sections.filter(s => s.type === type)
|
|
59
|
+
setPicks({ ...picks, [code]: { ...picks[code], [type]: (picks[code][type] + 1) % secs.length } })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// gather all visible meetings for calendar
|
|
63
|
+
const events: { code: string, meeting: Meeting, colorIdx: number }[] = []
|
|
64
|
+
const codes = Object.keys(added)
|
|
65
|
+
for (const [i, code] of codes.entries()) {
|
|
66
|
+
const c = added[code]
|
|
67
|
+
const p = picks[code] || {}
|
|
68
|
+
for (const type of Object.keys(p)) {
|
|
69
|
+
const secs = c.sections.filter(s => s.type === type)
|
|
70
|
+
const sec = secs[p[type]]
|
|
71
|
+
if (sec) for (const m of sec.meetings) events.push({ code, meeting: m, colorIdx: i })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return <App center={false} cn="p-4 h-screen">
|
|
76
|
+
<Grid cols={3} cn="h-full">
|
|
77
|
+
|
|
78
|
+
{/* calendar */}
|
|
79
|
+
<Card cn="col-span-2 row-span-1 p-4 overflow-hidden flex flex-col">
|
|
80
|
+
<D cn="grid flex-1" style={{ gridTemplateColumns: `60px repeat(5, 1fr)` }}>
|
|
81
|
+
{/* hour labels */}
|
|
82
|
+
<D cn="flex flex-col justify-between text-sm text-zinc-500 pr-2">
|
|
83
|
+
{hours.map(h => <span key={h}>{h > 12 ? h - 12 : h}{h >= 12 ? 'pm' : 'am'}</span>)}
|
|
84
|
+
</D>
|
|
85
|
+
{/* day columns */}
|
|
86
|
+
{days.map(day => <D key={day} cn="relative border-l border-zinc-800">
|
|
87
|
+
<Muted cn="text-center block text-sm mb-1">{day}</Muted>
|
|
88
|
+
{/* hour gridlines */}
|
|
89
|
+
{hours.map(h => <D key={h} cn="border-t border-zinc-800/50 left-0 right-0 absolute" style={{ top: `${h*8}%` }} />)}
|
|
90
|
+
{/* events */}
|
|
91
|
+
{events.filter(e => e.meeting.day === day).map((e, i) => {
|
|
92
|
+
const top = ((e.meeting.start - 540) / (13 * 60)) * 100
|
|
93
|
+
const height = ((e.meeting.end - e.meeting.start) / (13 * 60)) * 100
|
|
94
|
+
return <D key={i} cn={`absolute left-0.5 right-0.5 rounded text-sm px-1 text-white ${colors[e.colorIdx % colors.length]}`}
|
|
95
|
+
style={{ top: `${top}%`, height: `${height}%` }}>
|
|
96
|
+
{e.code}
|
|
97
|
+
</D>
|
|
98
|
+
})}
|
|
99
|
+
</D>)}
|
|
100
|
+
</D>
|
|
101
|
+
</Card>
|
|
102
|
+
|
|
103
|
+
{/* right panel */}
|
|
104
|
+
<D cn="flex flex-col gap-3">
|
|
105
|
+
{/* search */}
|
|
106
|
+
<Card cn="p-4">
|
|
107
|
+
<Row>
|
|
108
|
+
<Input grow placeholder="search..." value={query} onChange={e => setQuery(e.target.value)} />
|
|
109
|
+
<Select><option>2026W</option><option>2026S</option></Select>
|
|
110
|
+
</Row>
|
|
111
|
+
<D cn="space-y-3 max-h-48 overflow-y-auto flex-row">
|
|
112
|
+
{results.map(c => <Btn key={c.code} click={() => addCourse(c)} grow>
|
|
113
|
+
{c.code} — {c.name}
|
|
114
|
+
</Btn>)}
|
|
115
|
+
{query.length >= 2 && !results.length && <Muted>no results</Muted>}
|
|
116
|
+
</D>
|
|
117
|
+
</Card>
|
|
118
|
+
|
|
119
|
+
{/* added */}
|
|
120
|
+
<Card cn="p-4 space-y-3 flex-1">
|
|
121
|
+
{codes.map((code, i) => {
|
|
122
|
+
const c = added[code]
|
|
123
|
+
const types = [...new Set(c.sections.map(s => s.type))]
|
|
124
|
+
return <D key={code} cn="space-y-1">
|
|
125
|
+
<Row align='mid'>
|
|
126
|
+
<span className={`w-2 h-2 rounded-full ${colors[i % colors.length]}`} />
|
|
127
|
+
<Muted cn="flex-1">{code}</Muted>
|
|
128
|
+
<Chip click={() => removeCourse(code)}>×</Chip>
|
|
129
|
+
</Row>
|
|
130
|
+
<Row>
|
|
131
|
+
{types.map(t => {
|
|
132
|
+
const secs = c.sections.filter(s => s.type === t)
|
|
133
|
+
const idx = picks[code]?.[t] ?? 0
|
|
134
|
+
return <Chip key={t} click={() => cyclePick(code, t)}>
|
|
135
|
+
{t} {secs[idx]?.id}
|
|
136
|
+
</Chip>
|
|
137
|
+
})}
|
|
138
|
+
</Row>
|
|
139
|
+
</D>
|
|
140
|
+
})}
|
|
141
|
+
{!codes.length && <Muted>no courses added</Muted>}
|
|
142
|
+
</Card>
|
|
143
|
+
</D>
|
|
144
|
+
|
|
145
|
+
</Grid>
|
|
146
|
+
</App>
|
|
147
|
+
}
|