mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat: implement comments feature (#171)
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -11,10 +11,12 @@ export * from './form'
|
||||
export * from './hover-card'
|
||||
export * from './icons'
|
||||
export * from './lazy-image'
|
||||
export * from './mobile-tab'
|
||||
export * from './modal'
|
||||
export * from './portal'
|
||||
export * from './prompts'
|
||||
export * from './scroll-areas'
|
||||
export * from './segment'
|
||||
export * from './select'
|
||||
export * from './sonner'
|
||||
export * from './sonner'
|
||||
|
||||
8
packages/ui/src/mobile-tab/ctx.tsx
Normal file
8
packages/ui/src/mobile-tab/ctx.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
export interface MobileTabGroupContextValue {
|
||||
value: string
|
||||
setValue: (value: string) => void
|
||||
componentId: string
|
||||
}
|
||||
export const MobileTabGroupContext = createContext<MobileTabGroupContextValue>(null!)
|
||||
86
packages/ui/src/mobile-tab/index.tsx
Normal file
86
packages/ui/src/mobile-tab/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { clsxm, Spring } from '@afilmory/utils'
|
||||
import { m } from 'motion/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { use, useId, useMemo, useState } from 'react'
|
||||
|
||||
import { MobileTabGroupContext } from './ctx'
|
||||
|
||||
interface MobileTabGroupProps {
|
||||
value?: string
|
||||
onValueChanged?: (value: string) => void
|
||||
}
|
||||
|
||||
export const MobileTabGroup = (props: ComponentType<MobileTabGroupProps>) => {
|
||||
const { onValueChanged, value, className } = props
|
||||
|
||||
const [currentValue, setCurrentValue] = useState(value || '')
|
||||
const componentId = useId()
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @eslint-react/no-context-provider
|
||||
<MobileTabGroupContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
value: currentValue,
|
||||
setValue: (value) => {
|
||||
setCurrentValue(value)
|
||||
onValueChanged?.(value)
|
||||
},
|
||||
componentId,
|
||||
}),
|
||||
[componentId, currentValue, onValueChanged],
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="tablist"
|
||||
className={clsxm('relative flex items-center border-b border-accent/10 px-4 pb-3 pt-4', className)}
|
||||
tabIndex={0}
|
||||
data-orientation="horizontal"
|
||||
>
|
||||
<div className="flex flex-1 gap-1">{props.children}</div>
|
||||
</div>
|
||||
</MobileTabGroupContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const MobileTabItem: Component<{
|
||||
value: string
|
||||
label: ReactNode
|
||||
activeBgClassName?: string
|
||||
}> = ({ label, value, className, activeBgClassName }) => {
|
||||
const ctx = use(MobileTabGroupContext)
|
||||
const isActive = ctx.value === value
|
||||
const { setValue } = ctx
|
||||
const layoutId = ctx.componentId
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
className={clsxm(
|
||||
'relative flex flex-1 items-center justify-center gap-1.5 rounded-lg px-3 py-2 font-medium text-sm transition-colors',
|
||||
'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
|
||||
'focus-visible:ring-accent/30',
|
||||
isActive ? 'text-white' : 'text-white/60 hover:text-white/80',
|
||||
className,
|
||||
)}
|
||||
tabIndex={-1}
|
||||
data-orientation="horizontal"
|
||||
onClick={() => {
|
||||
setValue(value)
|
||||
}}
|
||||
data-state={isActive ? 'active' : 'inactive'}
|
||||
>
|
||||
<span className="z-[1]">{label}</span>
|
||||
|
||||
{isActive && (
|
||||
<m.div
|
||||
layout
|
||||
layoutId={layoutId}
|
||||
transition={Spring.presets.smooth}
|
||||
className={clsxm('absolute inset-x-0 bottom-0 h-0.5 bg-accent/60 rounded-full', activeBgClassName)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
8
packages/ui/src/segment/ctx.tsx
Normal file
8
packages/ui/src/segment/ctx.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
export interface SegmentGroupContextValue {
|
||||
value: string
|
||||
setValue: (value: string) => void
|
||||
componentId: string
|
||||
}
|
||||
export const SegmentGroupContext = createContext<SegmentGroupContextValue>(null!)
|
||||
85
packages/ui/src/segment/index.tsx
Normal file
85
packages/ui/src/segment/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { clsxm as cn, Spring } from '@afilmory/utils'
|
||||
import { m } from 'motion/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { use, useId, useMemo, useState } from 'react'
|
||||
|
||||
import { SegmentGroupContext } from './ctx'
|
||||
|
||||
interface SegmentGroupProps {
|
||||
value?: string
|
||||
onValueChanged?: (value: string) => void
|
||||
}
|
||||
export const SegmentGroup = (props: ComponentType<SegmentGroupProps>) => {
|
||||
const { onValueChanged, value, className } = props
|
||||
|
||||
const [currentValue, setCurrentValue] = useState(value || '')
|
||||
const componentId = useId()
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @eslint-react/no-context-provider
|
||||
<SegmentGroupContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
value: currentValue,
|
||||
setValue: (value) => {
|
||||
setCurrentValue(value)
|
||||
onValueChanged?.(value)
|
||||
},
|
||||
componentId,
|
||||
}),
|
||||
[componentId, currentValue, onValueChanged],
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="tablist"
|
||||
className={cn(
|
||||
'bg-fill-tertiary text-text-secondary inline-flex h-9 items-center justify-center rounded-lg p-1 outline-none',
|
||||
className,
|
||||
)}
|
||||
tabIndex={0}
|
||||
data-orientation="horizontal"
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</SegmentGroupContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const SegmentItem: Component<{
|
||||
value: string
|
||||
label: ReactNode
|
||||
activeBgClassName?: string
|
||||
}> = ({ label, value, className, activeBgClassName }) => {
|
||||
const ctx = use(SegmentGroupContext)
|
||||
const isActive = ctx.value === value
|
||||
const { setValue } = ctx
|
||||
const layoutId = ctx.componentId
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
className={cn(
|
||||
'ring-offset-background data-[state=active]:text-text relative inline-flex items-center justify-center px-3 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
|
||||
'focus-visible:ring-accent/30 h-full rounded-md',
|
||||
className,
|
||||
)}
|
||||
tabIndex={-1}
|
||||
data-orientation="horizontal"
|
||||
onClick={() => {
|
||||
setValue(value)
|
||||
}}
|
||||
data-state={isActive ? 'active' : 'inactive'}
|
||||
>
|
||||
<span className="z-[1]">{label}</span>
|
||||
|
||||
{isActive && (
|
||||
<m.span
|
||||
layout
|
||||
transition={Spring.presets.smooth}
|
||||
layoutId={layoutId}
|
||||
className={cn('absolute inset-0 z-0 rounded-md', activeBgClassName || 'bg-background')}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const SelectContent = ({
|
||||
className={cn(
|
||||
'bg-material-medium backdrop-blur-background text-text z-[60] max-h-96 min-w-32 overflow-hidden rounded border p-1',
|
||||
'shadow-context-menu',
|
||||
'motion-scale-in-75 motion-duration-150 text-body lg:animate-none',
|
||||
'motion-scale-in-75 motion-duration-150 text-sm lg:animate-none',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
|
||||
@@ -103,10 +103,12 @@ function buildExifTags(photo: PhotoManifestItem): string {
|
||||
} else {
|
||||
ss = `${exif.ExposureTime}s`
|
||||
}
|
||||
} else if (!ss.endsWith('s') && // If it's a string and doesn't end with s, append it?
|
||||
} else if (
|
||||
!ss.endsWith('s') && // If it's a string and doesn't end with s, append it?
|
||||
// Actually exiftool usually gives nice strings or numbers.
|
||||
// Let's just trust the value but ensure 's' suffix if it looks like a number
|
||||
!Number.isNaN(Number(ss))) {
|
||||
!Number.isNaN(Number(ss))
|
||||
) {
|
||||
ss = `${ss}s`
|
||||
}
|
||||
tags.push(`<exif:shutterSpeed>${ss}</exif:shutterSpeed>`)
|
||||
|
||||
Reference in New Issue
Block a user