Skip to content

Commit b223ebe

Browse files
Mixtapes (#13898)
1 parent db310be commit b223ebe

File tree

28 files changed

+2854
-64
lines changed

28 files changed

+2854
-64
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"dependencies": {
3737
"@amcharts/amcharts5": "^5.13.6",
3838
"@amcharts/amcharts5-geodata": "^5.1.5",
39+
"@dnd-kit/core": "^6.3.1",
40+
"@dnd-kit/sortable": "^10.0.0",
41+
"@dnd-kit/utilities": "^3.2.2",
3942
"@dotlottie/react-player": "^1.6.6",
4043
"@fontsource/source-code-pro": "^4.5.4",
4144
"@gatsbyjs/reach-router": "^1.3.9",

pnpm-lock.yaml

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@ packages:
33
- plugins/*
44

55
minimumReleaseAge: 1440
6+
7+
# Hoist Gatsby's internal dependencies for webpack compatibility
8+
publicHoistPattern:
9+
- "*webpack-hot-middleware*"
10+
- "*error-stack-parser*"
11+
- "*platform*"
12+
- "*css.escape*"
13+
- "*anser*"

src/components/AppWindow/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ export default function AppWindow({ item, chrome = true }: { item: AppWindowType
370370
const handleMouseDown = () => {
371371
if (focusedWindow === item) return
372372
if (item.path.startsWith('/')) {
373-
navigate(item.path, { state: { newWindow: true } })
373+
navigate(`${item.path}${item.location?.search || ''}`, { state: { newWindow: true } })
374374
} else {
375375
bringToFront(item)
376376
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import React from 'react'
2+
import { IconCheck, IconX } from '@posthog/icons'
3+
import OSButton from 'components/OSButton'
4+
5+
type SelectOption = {
6+
label: string
7+
value: any
8+
}
9+
10+
export default function CreatableMultiSelect({
11+
label,
12+
placeholder,
13+
description,
14+
options,
15+
value,
16+
onChange,
17+
onBlur,
18+
touched,
19+
error,
20+
allowCreate = true,
21+
onCreate,
22+
hideLabel = false,
23+
required = true,
24+
}: {
25+
label: string
26+
placeholder?: string
27+
description?: string
28+
options: SelectOption[]
29+
value: any[]
30+
onChange: (next: any[]) => void
31+
onBlur?: () => void
32+
touched?: boolean
33+
error?: string
34+
allowCreate?: boolean
35+
hideLabel?: boolean
36+
onCreate?: (value: string) => void
37+
required?: boolean
38+
}): JSX.Element {
39+
const [query, setQuery] = React.useState('')
40+
const [focused, setFocused] = React.useState(false)
41+
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
42+
const listRef = React.useRef<HTMLDivElement | null>(null)
43+
44+
const filtered = React.useMemo(() => {
45+
const q = query.trim().toLowerCase()
46+
if (!q) return options
47+
return options.filter((opt) => opt.label.toLowerCase().includes(q))
48+
}, [query, options])
49+
50+
React.useEffect(() => {
51+
if (!focused || filtered.length === 0) {
52+
setHighlightedIndex(-1)
53+
}
54+
}, [filtered, focused])
55+
56+
React.useEffect(() => {
57+
if (!listRef.current || highlightedIndex < 0) return
58+
const child = listRef.current.children[highlightedIndex] as HTMLElement | undefined
59+
child?.scrollIntoView({ block: 'nearest' })
60+
}, [highlightedIndex])
61+
62+
const addValue = (valueToAdd: any) => {
63+
if (!valueToAdd) return
64+
const next = Array.from(new Set([...(value || []), valueToAdd]))
65+
onChange(next)
66+
setQuery('')
67+
}
68+
69+
const removeValue = (item: any) => {
70+
onChange((value || []).filter((v) => v !== item))
71+
}
72+
73+
const createNewOption = (label: string) => {
74+
const normalized = label.trim()
75+
if (!normalized) return
76+
onCreate?.(normalized)
77+
addValue(normalized)
78+
}
79+
80+
const handleKeyDown = (e: React.KeyboardEvent) => {
81+
if (e.key === 'Enter') {
82+
e.preventDefault()
83+
if (highlightedIndex >= 0 && highlightedIndex < filtered.length) {
84+
addValue(filtered[highlightedIndex].value)
85+
} else if (query.trim()) {
86+
if (allowCreate) {
87+
createNewOption(query)
88+
} else {
89+
const candidate =
90+
filtered.find((opt) => opt.label.toLowerCase() === query.trim().toLowerCase()) || filtered[0]
91+
if (candidate) addValue(candidate.value)
92+
}
93+
}
94+
} else if (e.key === 'Backspace' && !query && value.length > 0) {
95+
removeValue(value[value.length - 1])
96+
} else if (e.key === 'ArrowDown') {
97+
e.preventDefault()
98+
setHighlightedIndex((prev) => (prev < filtered.length - 1 ? prev + 1 : 0))
99+
} else if (e.key === 'ArrowUp') {
100+
e.preventDefault()
101+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filtered.length - 1))
102+
} else if (e.key === 'Escape') {
103+
e.preventDefault()
104+
setFocused(false)
105+
}
106+
}
107+
108+
const canCreate =
109+
allowCreate && query.trim() && !options.some((o) => o.label.toLowerCase() === query.trim().toLowerCase())
110+
111+
return (
112+
<div className="flex flex-col space-y-1">
113+
{!hideLabel && (
114+
<div className="w-full">
115+
<label className="text-[15px]">
116+
<span>
117+
{label}
118+
{required && <span className="text-red dark:text-yellow ml-0.5">*</span>}
119+
</span>
120+
</label>
121+
{description && <p className="text-sm text-secondary m-0 mt-0.5">{description}</p>}
122+
</div>
123+
)}
124+
<div
125+
className={`bg-primary border rounded ring-0 px-2.5 py-1 ${
126+
touched && error ? 'border-red dark:border-yellow' : 'border-primary'
127+
}`}
128+
onMouseDown={(e) => {
129+
if ((e.target as HTMLElement).tagName !== 'INPUT') {
130+
e.preventDefault()
131+
}
132+
}}
133+
>
134+
<div className="flex flex-wrap gap-1">
135+
{(value || []).map((v) => {
136+
const chip = options.find((o) => o.value === v) || { label: String(v), value: v }
137+
return (
138+
<span
139+
key={String(v)}
140+
className="inline-flex items-center gap-1 px-2 py-0.5 rounded border border-primary text-xs bg-accent h-[28px]"
141+
>
142+
<span>{chip.label}</span>
143+
<button
144+
type="button"
145+
aria-label={`Remove ${String(v)}`}
146+
onClick={() => removeValue(v)}
147+
className="text-secondary hover:text-primary size-3"
148+
>
149+
<IconX />
150+
</button>
151+
</span>
152+
)
153+
})}
154+
<input
155+
className="flex-1 min-w-[8rem] bg-transparent outline-none border-0 ring-0 focus:ring-0 text-[15px] px-0 py-0.5"
156+
placeholder={placeholder || label}
157+
value={query}
158+
onChange={(e) => setQuery(e.target.value)}
159+
onFocus={() => setFocused(true)}
160+
onBlur={() => {
161+
setTimeout(() => setFocused(false), 200)
162+
onBlur?.()
163+
}}
164+
onKeyDown={handleKeyDown}
165+
/>
166+
</div>
167+
{filtered.length > 0 && focused && (
168+
<div
169+
ref={listRef}
170+
role="listbox"
171+
className="mt-1 max-h-40 overflow-auto rounded border border-primary bg-primary"
172+
>
173+
{filtered.map((opt, idx) => {
174+
const isSelected = (value || []).includes(opt.value)
175+
return (
176+
<button
177+
type="button"
178+
role="option"
179+
aria-selected={idx === highlightedIndex}
180+
key={`${String(opt.value)}-${opt.label}`}
181+
className={`block w-full text-left px-2.5 py-1 text-sm ${
182+
idx === highlightedIndex ? 'bg-accent' : 'hover:bg-accent'
183+
}`}
184+
onMouseEnter={() => setHighlightedIndex(idx)}
185+
onMouseDown={(e) => {
186+
e.preventDefault()
187+
if (isSelected) {
188+
removeValue(opt.value)
189+
} else {
190+
addValue(opt.value)
191+
}
192+
}}
193+
>
194+
<span className="flex items-center justify-between gap-2">
195+
<span>{opt.label}</span>
196+
{isSelected && <IconCheck className="size-4 text-primary opacity-80" />}
197+
</span>
198+
</button>
199+
)
200+
})}
201+
</div>
202+
)}
203+
{canCreate && (
204+
<div className="mt-1">
205+
<OSButton
206+
size="sm"
207+
variant="default"
208+
onMouseDown={(e) => {
209+
e.preventDefault()
210+
createNewOption(query)
211+
}}
212+
>
213+
Add "{query.trim()}"
214+
</OSButton>
215+
</div>
216+
)}
217+
</div>
218+
{touched && error && <p className="text-sm text-red dark:text-yellow m-0 mt-1">{error}</p>}
219+
</div>
220+
)
221+
}

0 commit comments

Comments
 (0)