feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,62 @@
|
|||
import type { VaultEntry } from '../../types'
|
||||
import type { RetargetOption } from './RetargetNoteDialog'
|
||||
import { RetargetNoteDialog } from './RetargetNoteDialog'
|
||||
|
||||
interface NoteRetargetingDialogsProps {
|
||||
dialogState: { kind: 'type' | 'folder'; notePath: string } | null
|
||||
dialogEntry: VaultEntry | null
|
||||
typeOptions: RetargetOption[]
|
||||
folderOptions: RetargetOption[]
|
||||
onClose: () => void
|
||||
onSelectType: (type: string) => boolean | Promise<boolean>
|
||||
onSelectFolder: (folderPath: string) => boolean | Promise<boolean>
|
||||
}
|
||||
|
||||
function typeDialogDescription(entry: VaultEntry | null): string {
|
||||
return entry
|
||||
? `Set a new type for "${entry.title}".`
|
||||
: 'Select a type for the active note.'
|
||||
}
|
||||
|
||||
function folderDialogDescription(entry: VaultEntry | null): string {
|
||||
return entry
|
||||
? `Choose a destination folder for "${entry.title}".`
|
||||
: 'Select a destination folder for the active note.'
|
||||
}
|
||||
|
||||
export function NoteRetargetingDialogs({
|
||||
dialogState,
|
||||
dialogEntry,
|
||||
typeOptions,
|
||||
folderOptions,
|
||||
onClose,
|
||||
onSelectType,
|
||||
onSelectFolder,
|
||||
}: NoteRetargetingDialogsProps) {
|
||||
return (
|
||||
<>
|
||||
<RetargetNoteDialog
|
||||
open={dialogState?.kind === 'type'}
|
||||
title="Change Note Type"
|
||||
description={typeDialogDescription(dialogEntry)}
|
||||
searchPlaceholder="Search types"
|
||||
emptyMessage="No other note types available."
|
||||
options={typeOptions}
|
||||
onClose={onClose}
|
||||
onSelect={onSelectType}
|
||||
testIdPrefix="retarget-note-type"
|
||||
/>
|
||||
<RetargetNoteDialog
|
||||
open={dialogState?.kind === 'folder'}
|
||||
title="Move Note to Folder"
|
||||
description={folderDialogDescription(dialogEntry)}
|
||||
searchPlaceholder="Search folders"
|
||||
emptyMessage="No other folders available."
|
||||
options={folderOptions}
|
||||
onClose={onClose}
|
||||
onSelect={onSelectFolder}
|
||||
testIdPrefix="retarget-note-folder"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import { Check, StackSimple } from '@phosphor-icons/react'
|
||||
import { useMemo, useState, type KeyboardEvent } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface RetargetOption {
|
||||
id: string
|
||||
label: string
|
||||
detail?: string
|
||||
current?: boolean
|
||||
}
|
||||
|
||||
interface RetargetNoteDialogProps {
|
||||
open: boolean
|
||||
title: string
|
||||
description: string
|
||||
searchPlaceholder: string
|
||||
emptyMessage: string
|
||||
options: RetargetOption[]
|
||||
onClose: () => void
|
||||
onSelect: (id: string) => boolean | Promise<boolean>
|
||||
testIdPrefix: string
|
||||
}
|
||||
|
||||
function matchesQuery(option: RetargetOption, query: string): boolean {
|
||||
const normalized = query.trim().toLowerCase()
|
||||
if (!normalized) return true
|
||||
return option.label.toLowerCase().includes(normalized)
|
||||
|| option.detail?.toLowerCase().includes(normalized)
|
||||
|| false
|
||||
}
|
||||
|
||||
function initialHighlightIndex(options: RetargetOption[]): number {
|
||||
if (options.length === 0) return -1
|
||||
const currentIndex = options.findIndex((option) => option.current)
|
||||
return currentIndex >= 0 ? currentIndex : 0
|
||||
}
|
||||
|
||||
function nextHighlightIndex(current: number, total: number, direction: 'next' | 'previous'): number {
|
||||
if (total === 0) return -1
|
||||
if (current < 0) return direction === 'next' ? 0 : total - 1
|
||||
return direction === 'next'
|
||||
? (current + 1) % total
|
||||
: (current - 1 + total) % total
|
||||
}
|
||||
|
||||
export function RetargetNoteDialog({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
searchPlaceholder,
|
||||
emptyMessage,
|
||||
options,
|
||||
onClose,
|
||||
onSelect,
|
||||
testIdPrefix,
|
||||
}: RetargetNoteDialogProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() => options.filter((option) => matchesQuery(option, query)),
|
||||
[options, query],
|
||||
)
|
||||
|
||||
const effectiveHighlightedIndex = highlightedIndex >= 0 && highlightedIndex < filteredOptions.length
|
||||
? highlightedIndex
|
||||
: initialHighlightIndex(filteredOptions)
|
||||
|
||||
const resetDialogState = () => {
|
||||
setQuery('')
|
||||
setHighlightedIndex(-1)
|
||||
}
|
||||
|
||||
const submitSelection = async (optionId: string) => {
|
||||
const shouldClose = await onSelect(optionId)
|
||||
if (!shouldClose) return
|
||||
resetDialogState()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
setHighlightedIndex((current) => nextHighlightIndex(current, filteredOptions.length, 'next'))
|
||||
return
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
setHighlightedIndex((current) => nextHighlightIndex(current, filteredOptions.length, 'previous'))
|
||||
return
|
||||
}
|
||||
if (event.key === 'Enter' && effectiveHighlightedIndex >= 0) {
|
||||
event.preventDefault()
|
||||
const highlightedOption = filteredOptions.at(effectiveHighlightedIndex)
|
||||
if (highlightedOption) void submitSelection(highlightedOption.id)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen) return
|
||||
resetDialogState()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-xl gap-3" showCloseButton={true}>
|
||||
<DialogHeader className="gap-1">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder={searchPlaceholder}
|
||||
data-testid={`${testIdPrefix}-search`}
|
||||
/>
|
||||
<ScrollArea className="max-h-80 rounded-md border">
|
||||
{filteredOptions.length === 0 ? (
|
||||
<div className="px-4 py-6 text-sm text-muted-foreground" data-testid={`${testIdPrefix}-empty`}>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-1" data-testid={`${testIdPrefix}-options`}>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<Button
|
||||
key={option.id}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'h-auto w-full justify-start rounded-md px-3 py-2 text-left',
|
||||
effectiveHighlightedIndex === index && 'bg-accent text-accent-foreground',
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-option:${option.id}`}
|
||||
onMouseMove={() => setHighlightedIndex(index)}
|
||||
onClick={() => { void submitSelection(option.id) }}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-start gap-2">
|
||||
<span className="mt-0.5 shrink-0 text-muted-foreground">
|
||||
{option.current ? <Check size={14} weight="bold" /> : <StackSimple size={14} />}
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate text-sm font-medium text-foreground">{option.label}</span>
|
||||
{option.detail && (
|
||||
<span className="truncate text-xs text-muted-foreground">{option.detail}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{option.current && (
|
||||
<span className="shrink-0 text-xs font-medium text-muted-foreground">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue