@hogoshi/ui 0.1.10 → 0.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogoshi/ui",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,14 +4,14 @@ import {
4
4
  Alert01Icon,
5
5
  ArrowDown01Icon,
6
6
  ArrowUp02Icon,
7
+ Attachment01Icon,
7
8
  Cancel01Icon,
8
9
  File01Icon,
9
10
  FlashIcon,
10
- Pin02Icon,
11
11
  StopCircleIcon,
12
12
  Tick02Icon,
13
+ Upload04Icon,
13
14
  } from "@hugeicons-pro/core-stroke-rounded";
14
- import { Pin02Icon as Pin02SolidIcon } from "@hugeicons-pro/core-solid-rounded";
15
15
  import { useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react";
16
16
  import { cn } from "../lib/cn";
17
17
  import type { AppIcon } from "./icon";
@@ -20,6 +20,7 @@ import { Image, ImageGallery, ImageViewer, type ViewerImage } from "./image-view
20
20
  import { Button } from "./button";
21
21
  import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./collapsible";
22
22
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu";
23
+ import { Separator } from "./separator";
23
24
  import { Spinner } from "./spinner";
24
25
 
25
26
  const workingText =
@@ -277,7 +278,6 @@ export interface ChatContextItem {
277
278
  label: string;
278
279
  src?: string;
279
280
  icon?: AppIcon;
280
- pinned?: boolean;
281
281
  }
282
282
 
283
283
  export interface ChatModelOption {
@@ -286,15 +286,7 @@ export interface ChatModelOption {
286
286
  hint?: string;
287
287
  }
288
288
 
289
- function ContextChip({
290
- item,
291
- onTogglePin,
292
- onRemove,
293
- }: {
294
- item: ChatContextItem;
295
- onTogglePin?: () => void;
296
- onRemove?: () => void;
297
- }) {
289
+ function ContextChip({ item, onRemove }: { item: ChatContextItem; onRemove?: () => void }) {
298
290
  return (
299
291
  <span className="border-border bg-bg-subtle text-fg inline-flex h-7 max-w-[16rem] min-w-0 items-center gap-1.5 rounded-full border py-1 pr-1 pl-2.5">
300
292
  {item.src ? (
@@ -303,20 +295,6 @@ function ContextChip({
303
295
  <Icon icon={item.icon ?? File01Icon} variant="stroke" size={12} className="text-fg-muted shrink-0" />
304
296
  )}
305
297
  <span className="min-w-0 truncate text-xs">{item.label}</span>
306
- {onTogglePin ? (
307
- <button
308
- type="button"
309
- aria-label={item.pinned ? `Unpin ${item.label}` : `Pin ${item.label}`}
310
- aria-pressed={item.pinned}
311
- onClick={onTogglePin}
312
- className={cn(
313
- "text-fg-muted hover:text-fg focus-visible:ring-ring inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full outline-none focus-visible:ring-2",
314
- item.pinned && "text-accent",
315
- )}
316
- >
317
- <Icon icon={item.pinned ? Pin02SolidIcon : Pin02Icon} size={12} />
318
- </button>
319
- ) : null}
320
298
  {onRemove ? (
321
299
  <button
322
300
  type="button"
@@ -347,8 +325,10 @@ function ModelMenu({
347
325
  <Button size="sm" variant="ghost" aria-label="Select model" className="text-fg-muted h-7 px-1.5" />
348
326
  }
349
327
  >
350
- <Icon icon={FlashIcon} variant="stroke" size={14} className="text-fg-subtle" />
351
- <span className="max-w-40 truncate text-xs">{model.label}</span>
328
+ <span className="inline-flex items-center gap-1.5">
329
+ <Icon icon={FlashIcon} variant="stroke" size={14} className="text-fg-subtle shrink-0" />
330
+ <span className="max-w-40 truncate text-xs">{model.label}</span>
331
+ </span>
352
332
  </DropdownMenuTrigger>
353
333
  <DropdownMenuContent>
354
334
  {models.map((option) => (
@@ -367,6 +347,65 @@ function ModelMenu({
367
347
  );
368
348
  }
369
349
 
350
+ export function ChatAttach({
351
+ items = [],
352
+ onInsert,
353
+ onUpload,
354
+ accept,
355
+ multiple = true,
356
+ className,
357
+ }: {
358
+ items?: ChatSuggestItem[];
359
+ onInsert?: (id: string) => void;
360
+ onUpload?: (files: File[]) => void;
361
+ accept?: string;
362
+ multiple?: boolean;
363
+ className?: string;
364
+ }) {
365
+ const input = useRef<HTMLInputElement>(null);
366
+ return (
367
+ <>
368
+ <input
369
+ ref={input}
370
+ type="file"
371
+ accept={accept}
372
+ multiple={multiple}
373
+ className="hidden"
374
+ onChange={(event) => {
375
+ const files = [...(event.target.files ?? [])];
376
+ event.target.value = "";
377
+ if (files.length) onUpload?.(files);
378
+ }}
379
+ />
380
+ <DropdownMenu>
381
+ <DropdownMenuTrigger render={<Button size="icon-sm" variant="ghost" aria-label="Attach" className={className} />}>
382
+ <Icon icon={Attachment01Icon} variant="stroke" />
383
+ </DropdownMenuTrigger>
384
+ <DropdownMenuContent>
385
+ {items.length ? (
386
+ <>
387
+ <p className="text-fg-subtle px-2 py-1.5 text-[11px] font-medium tracking-wider uppercase">Project files</p>
388
+ <div className="max-h-60 overflow-y-auto overscroll-contain">
389
+ {items.map((item) => (
390
+ <DropdownMenuItem key={item.id} onClick={() => onInsert?.(item.id)}>
391
+ <Icon icon={item.icon ?? File01Icon} variant="stroke" size={16} className="text-fg-muted shrink-0" />
392
+ <span className="min-w-0 truncate">{item.label}</span>
393
+ </DropdownMenuItem>
394
+ ))}
395
+ </div>
396
+ <Separator className="my-1" />
397
+ </>
398
+ ) : null}
399
+ <DropdownMenuItem onClick={() => input.current?.click()}>
400
+ <Icon icon={Upload04Icon} variant="stroke" size={16} className="text-fg-muted shrink-0" />
401
+ Upload from computer
402
+ </DropdownMenuItem>
403
+ </DropdownMenuContent>
404
+ </DropdownMenu>
405
+ </>
406
+ );
407
+ }
408
+
370
409
  export function ChatComposer({
371
410
  value,
372
411
  onValueChange,
@@ -377,7 +416,6 @@ export function ChatComposer({
377
416
  placeholder,
378
417
  disabled,
379
418
  context,
380
- onToggleContextPin,
381
419
  onRemoveContext,
382
420
  model,
383
421
  models,
@@ -394,7 +432,6 @@ export function ChatComposer({
394
432
  placeholder?: string;
395
433
  disabled?: boolean;
396
434
  context?: ChatContextItem[];
397
- onToggleContextPin?: (id: string) => void;
398
435
  onRemoveContext?: (id: string) => void;
399
436
  model?: ChatModelOption;
400
437
  models?: ChatModelOption[];
@@ -424,7 +461,6 @@ export function ChatComposer({
424
461
  <ContextChip
425
462
  key={item.id}
426
463
  item={item}
427
- onTogglePin={onToggleContextPin ? () => onToggleContextPin(item.id) : undefined}
428
464
  onRemove={onRemoveContext ? () => onRemoveContext(item.id) : undefined}
429
465
  />
430
466
  ))}
@@ -559,14 +595,10 @@ export function ChatSuggest({
559
595
 
560
596
  export function ChatMedia({
561
597
  images,
562
- pinned = false,
563
- onPin,
564
598
  actions,
565
599
  className,
566
600
  }: {
567
601
  images: ViewerImage[];
568
- pinned?: boolean;
569
- onPin?: () => void;
570
602
  actions?: ReactNode;
571
603
  className?: string;
572
604
  }) {
@@ -579,32 +611,13 @@ export function ChatMedia({
579
611
  src={images[0].src}
580
612
  alt={images[0].alt}
581
613
  name={images[0].name}
582
- className="border-border max-h-60 w-auto max-w-full rounded-[var(--radius-md)] border object-cover"
614
+ className="border-border max-h-48 w-auto max-w-full rounded-[var(--radius-md)] border object-cover"
583
615
  />
584
616
  ) : (
585
617
  <ImageGallery images={images} className="min-w-0" />
586
618
  )}
587
- {onPin || actions ? (
588
- <div
589
- className={cn(
590
- "absolute top-2 right-2 flex items-center gap-1 transition-opacity duration-[var(--motion-duration)] ease-[var(--motion-ease)] focus-within:opacity-100 group-hover/media:opacity-100",
591
- pinned ? "opacity-100" : "opacity-0",
592
- )}
593
- >
594
- {onPin ? (
595
- <button
596
- type="button"
597
- aria-label={pinned ? "Unpin from context" : "Pin to context"}
598
- aria-pressed={pinned}
599
- onClick={onPin}
600
- className={cn(
601
- "border-border bg-bg-elevated/80 text-fg-muted hover:text-fg pointer-events-auto inline-flex size-7 cursor-pointer items-center justify-center rounded-[var(--radius-sm)] border backdrop-blur-md",
602
- pinned && "text-accent",
603
- )}
604
- >
605
- <Icon icon={pinned ? Pin02SolidIcon : Pin02Icon} size={14} />
606
- </button>
607
- ) : null}
619
+ {actions ? (
620
+ <div className="absolute top-2 right-2 flex items-center gap-1 opacity-0 transition-opacity duration-[var(--motion-duration)] ease-[var(--motion-ease)] focus-within:opacity-100 group-hover/media:opacity-100">
608
621
  {actions}
609
622
  </div>
610
623
  ) : null}
@@ -80,16 +80,21 @@ function editEntries(): ContextMenuEntry[] {
80
80
  export const ContextMenuRoot = BaseContextMenu.Root;
81
81
  export const ContextMenuTrigger = BaseContextMenu.Trigger;
82
82
 
83
- export function ContextMenuContent({ className, children, ...props }: ComponentProps<typeof BaseContextMenu.Popup>) {
83
+ export function ContextMenuContent({
84
+ edit = true,
85
+ className,
86
+ children,
87
+ ...props
88
+ }: ComponentProps<typeof BaseContextMenu.Popup> & { edit?: boolean }) {
84
89
  return (
85
90
  <BaseContextMenu.Portal>
86
91
  <BaseContextMenu.Positioner className="z-[var(--z-popover)]">
87
92
  <BaseContextMenu.Popup className={cn(popupClass, className)} {...props}>
88
93
  <Glide selector={HIGHLIGHTED} />
89
- <EntryList items={editEntries()} />
94
+ {edit ? <EntryList items={editEntries()} /> : null}
90
95
  {children ? (
91
96
  <>
92
- <Separator className="my-1" />
97
+ {edit ? <Separator className="my-1" /> : null}
93
98
  {children}
94
99
  </>
95
100
  ) : null}
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export {
22
22
  export { Card, CardDescription, CardHeader, CardTitle } from "./components/card";
23
23
  export {
24
24
  ChatActions,
25
+ ChatAttach,
25
26
  ChatComposer,
26
27
  ChatDiff,
27
28
  ChatMedia,