diff --git a/.cursor/rules/codebase-index.mdc b/.cursor/rules/codebase-index.mdc index 9b2d6585..0d576bc6 100644 --- a/.cursor/rules/codebase-index.mdc +++ b/.cursor/rules/codebase-index.mdc @@ -18,9 +18,6 @@ When implementing new features: ```typescript ## apps/web/src/constants -captions-constants.ts - export const LANGUAGES: Language[] - editor-constants.ts export const PLATFORM_LAYOUTS: Record export const PANEL_CONFIG @@ -44,6 +41,9 @@ font-constants.ts export const getGoogleFonts = (): FontOption[] => ... export const getSystemFonts = (): FontOption[] => ... +language-constants.ts + export const LANGUAGES + project-constants.ts export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] export const FPS_PRESETS @@ -57,27 +57,27 @@ site-constants.ts export const SITE_URL export const SITE_INFO export type ExternalTool = { - name: string; - description: string; - url: string; - icon: React.ElementType; + name: string; + description: string; + url: string; + icon: React.ElementType; } export const EXTERNAL_TOOLS: ExternalTool[] export const DEFAULT_LOGO_URL export const SOCIAL_LINKS export type Sponsor = { - name: string; - url: string; - logo: string; - description: string; + name: string; + url: string; + logo: string; + description: string; } export const SPONSORS: Sponsor[] stickers-constants.ts export const STICKER_CATEGORIES export const STICKER_CATEGORY_CONFIG: Record< - (typeof STICKER_CATEGORIES)[number], - string | undefined + (typeof STICKER_CATEGORIES)[number], + string | undefined > text-constants.ts @@ -91,6 +91,7 @@ timeline-constants.tsx export const TRACK_ICONS: Record transcription-constants.ts + export const TRANSCRIPTION_LANGUAGES export const TRANSCRIPTION_MODELS: TranscriptionModel[] export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId export const DEFAULT_CHUNK_LENGTH_SECONDS @@ -122,18 +123,18 @@ use-editor.ts use-file-upload.ts export function useFileUpload({ - accept, - multiple, - onFilesSelected, + accept, + multiple, + onFilesSelected, }: UseFileUploadOptions = {}) use-infinite-scroll.ts export function useInfiniteScroll({ - onLoadMore, - hasMore, - isLoading, - threshold = 200, - enabled = true, + onLoadMore, + hasMore, + isLoading, + threshold = 200, + enabled = true, }: UseInfiniteScrollOptions) use-keybindings.ts @@ -152,32 +153,35 @@ use-keyboard-shortcuts-help.ts } export function useKeyboardShortcutsHelp() +use-mobile.ts + export function useIsMobile() + use-raf-loop.ts export function useRafLoop(callback: ({ time }: { time: number }) => void) use-reveal-item.ts export function useRevealItem( - highlightId: string | null, - onClearHighlight: () => void, - highlightDuration = 1000, + highlightId: string | null, + onClearHighlight: () => void, + highlightDuration = 1000, ) use-sound-search.ts export function useSoundSearch({ - query, - commercialOnly, + query, + commercialOnly, }: { - query: string; - commercialOnly: boolean; + query: string; + commercialOnly: boolean; }) ## apps/web/src/hooks/actions use-action-handler.ts export function useActionHandler( - action: A, - handler: TActionFunc, - isActive: TActionHandlerOptions, + action: A, + handler: TActionFunc, + isActive: TActionHandlerOptions, ) use-editor-actions.ts @@ -187,69 +191,69 @@ use-editor-actions.ts use-edge-auto-scroll.ts export function useEdgeAutoScroll({ - isActive, - getMouseClientX, - rulerScrollRef, - tracksScrollRef, - contentWidth, - edgeThreshold = 100, - maxScrollSpeed = 15, + isActive, + getMouseClientX, + rulerScrollRef, + tracksScrollRef, + contentWidth, + edgeThreshold = 100, + maxScrollSpeed = 15, }: UseEdgeAutoScrollParams): void use-scroll-sync.ts export function useScrollSync({ - rulerScrollRef, - tracksScrollRef, - trackLabelsScrollRef, - bookmarksScrollRef, + rulerScrollRef, + tracksScrollRef, + trackLabelsScrollRef, + bookmarksScrollRef, }: UseScrollSyncProps) use-selection-box.ts export function useSelectionBox({ - containerRef, - onSelectionComplete, - isEnabled = true, - tracksScrollRef, - zoomLevel, + containerRef, + onSelectionComplete, + isEnabled = true, + tracksScrollRef, + zoomLevel, }: UseSelectionBoxProps) use-snap-indicator-position.ts export function useSnapIndicatorPosition({ - snapPoint, - zoomLevel, - tracks, - timelineRef, - trackLabelsRef, - tracksScrollRef, + snapPoint, + zoomLevel, + tracks, + timelineRef, + trackLabelsRef, + tracksScrollRef, }: UseSnapIndicatorPositionParams): SnapIndicatorPosition use-timeline-drag-drop.ts export function useTimelineDragDrop({ - containerRef, - zoomLevel, - isSnappingEnabled = true, + containerRef, + zoomLevel, + isSnappingEnabled = true, }: UseTimelineDragDropProps) use-timeline-playhead.ts export function useTimelinePlayhead({ - zoomLevel, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, + zoomLevel, + rulerRef, + rulerScrollRef, + tracksScrollRef, + playheadRef, }: UseTimelinePlayheadProps) use-timeline-seek.ts export function useTimelineSeek({ - playheadRef, - trackLabelsRef, - rulerScrollRef, - tracksScrollRef, - zoomLevel, - duration, - isSelecting, - clearSelectedElements, - seek, + playheadRef, + trackLabelsRef, + rulerScrollRef, + tracksScrollRef, + zoomLevel, + duration, + isSelecting, + clearSelectedElements, + seek, }: UseTimelineSeekProps) use-timeline-snapping.ts @@ -270,28 +274,28 @@ use-timeline-snapping.ts enablePlayheadSnapping?: boolean } export function useTimelineSnapping({ - snapThreshold = 10, - enableElementSnapping = true, - enablePlayheadSnapping = true, + snapThreshold = 10, + enableElementSnapping = true, + enablePlayheadSnapping = true, }: UseTimelineSnappingOptions = {}) use-timeline-zoom.ts export function useTimelineZoom({ - containerRef, - isInTimeline = false, - minZoom = TIMELINE_CONSTANTS.ZOOM_MIN, + containerRef, + isInTimeline = false, + minZoom = TIMELINE_CONSTANTS.ZOOM_MIN, }: UseTimelineZoomProps): UseTimelineZoomReturn ## apps/web/src/hooks/timeline/element use-element-interaction.ts export function useElementInteraction({ - zoomLevel, - timelineRef, - tracksContainerRef, - tracksScrollRef, - snappingEnabled, - onSnapPointChange, + zoomLevel, + timelineRef, + tracksContainerRef, + tracksScrollRef, + snappingEnabled, + onSnapPointChange, }: UseElementInteractionProps) use-element-resize.ts @@ -305,11 +309,11 @@ use-element-resize.ts initialDuration: number } export function useTimelineElementResize({ - element, - track, - zoomLevel, - onSnapPointChange, - onResizeStateChange, + element, + track, + zoomLevel, + onSnapPointChange, + onResizeStateChange, }: UseTimelineElementResizeProps) use-element-selection.ts @@ -319,34 +323,34 @@ use-element-selection.ts drag-data.ts export function setDragData({ - dataTransfer, - dragData, + dataTransfer, + dragData, }: { - dataTransfer: DataTransfer; - dragData: TimelineDragData; + dataTransfer: DataTransfer; + dragData: TimelineDragData; }): void export function getDragData({ - dataTransfer, + dataTransfer, }: { - dataTransfer: DataTransfer; + dataTransfer: DataTransfer; }): TimelineDragData | null export function hasDragData({ - dataTransfer, + dataTransfer, }: { - dataTransfer: DataTransfer; + dataTransfer: DataTransfer; }): boolean export function clearDragData(): void export.ts export function getExportMimeType({ - format, + format, }: { - format: ExportFormat; + format: ExportFormat; }): string export function getExportFileExtension({ - format, + format, }: { - format: ExportFormat; + format: ExportFormat; }): string iconify-api.ts @@ -356,14 +360,14 @@ iconify-api.ts name: string total: number author?: { - name: string; - url?: string; - } + name: string; + url?: string; + } license?: { - title: string; - spdx?: string; - url?: string; - } + title: string; + spdx?: string; + url?: string; + } samples?: string[] category?: string palette?: boolean @@ -385,40 +389,40 @@ iconify-api.ts aliases?: Record } export function getCollections( - category?: string, + category?: string, ): Promise> export function getCollection( - prefix: string, + prefix: string, ): Promise export function searchIcons( - query: string, - limit: number = 64, - prefixes?: string[], - category?: string, + query: string, + limit: number = 64, + prefixes?: string[], + category?: string, ): Promise export function buildIconSvgUrl( - host: string, - iconName: string, - params?: { - color?: string; - width?: number; - height?: number; - flip?: "horizontal" | "vertical" | "horizontal,vertical"; - rotate?: number | string; - }, + host: string, + iconName: string, + params?: { + color?: string; + width?: number; + height?: number; + flip?: "horizontal" | "vertical" | "horizontal,vertical"; + rotate?: number | string; + }, ): string export function getIconSvgUrl( - iconName: string, - params?: Parameters[2], + iconName: string, + params?: Parameters[2], ): string export function downloadSvgAsText( - iconName: string, - params?: Parameters[1], + iconName: string, + params?: Parameters[1], ): Promise export function svgToFile(svgText: string, fileName: string): File export const POPULAR_COLLECTIONS export function getCategoriesFromCollections( - collections: Record, + collections: Record, ): string[] rate-limit.ts @@ -429,114 +433,114 @@ scenes.ts export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] export function buildDefaultScene({ - name, - isMain, + name, + isMain, }: { - name: string; - isMain: boolean; + name: string; + isMain: boolean; }): TScene export function canDeleteScene({ scene }: { scene: TScene }): { - canDelete: boolean; - reason?: string; + canDelete: boolean; + reason?: string; } export function getFallbackSceneAfterDelete({ - scenes, - deletedSceneId, - currentSceneId, + scenes, + deletedSceneId, + currentSceneId, }: { - scenes: TScene[]; - deletedSceneId: string; - currentSceneId: string | null; + scenes: TScene[]; + deletedSceneId: string; + currentSceneId: string | null; }): TScene | null export function findCurrentScene({ - scenes, - currentSceneId, + scenes, + currentSceneId, }: { - scenes: TScene[]; - currentSceneId: string; + scenes: TScene[]; + currentSceneId: string; }): TScene | null export function updateSceneInArray({ - scenes, - sceneId, - updates, + scenes, + sceneId, + updates, }: { - scenes: TScene[]; - sceneId: string; - updates: Partial; + scenes: TScene[]; + sceneId: string; + updates: Partial; }): TScene[] time.ts export function roundToFrame({ - time, - fps, + time, + fps, }: { - time: number; - fps: number; + time: number; + fps: number; }): number export function formatTimeCode({ - timeInSeconds, - format = "HH:MM:SS:CS", - fps, + timeInSeconds, + format = "HH:MM:SS:CS", + fps, }: { - timeInSeconds: number; - format?: TTimeCode; - fps?: number; + timeInSeconds: number; + format?: TTimeCode; + fps?: number; }): string export function parseTimeCode({ - timeCode, - format = "HH:MM:SS:CS", - fps, + timeCode, + format = "HH:MM:SS:CS", + fps, }: { - timeCode: string; - format?: TTimeCode; - fps: number; + timeCode: string; + format?: TTimeCode; + fps: number; }): number | null export function guessTimeCodeFormat({ - timeCode, + timeCode, }: { - timeCode: string; + timeCode: string; }): TTimeCode | null export function timeToFrame({ - time, - fps, + time, + fps, }: { - time: number; - fps: number; + time: number; + fps: number; }): number export function frameToTime({ - frame, - fps, + frame, + fps, }: { - frame: number; - fps: number; + frame: number; + fps: number; }): number export function snapTimeToFrame({ - time, - fps, + time, + fps, }: { - time: number; - fps: number; + time: number; + fps: number; }): number export function getSnappedSeekTime({ - rawTime, - duration, - fps, + rawTime, + duration, + fps, }: { - rawTime: number; - duration: number; - fps: number; + rawTime: number; + duration: number; + fps: number; }): number ## apps/web/src/lib/actions definitions.ts export type TActionCategory = | "playback" - | "navigation" - | "editing" - | "selection" - | "history" - | "timeline" - | "con... + | "navigation" + | "editing" + | "selection" + | "history" + | "timeline" + | "controls" export interface TActionDefinition { description: string category: TActionCategory @@ -550,40 +554,40 @@ definitions.ts registry.ts export function bindAction( - action: A, - handler: TActionFunc, + action: A, + handler: TActionFunc, ) export function unbindAction( - action: A, - handler: TActionFunc, + action: A, + handler: TActionFunc, ) export const invokeAction = ( - action: A, - args?: TArgOfAction, - trigger?: TInvocationTrigger, + action: A, + args?: TArgOfAction, + trigger?: TInvocationTrigger, ) => ... types.ts export type TActionArgsMap = { - "seek-forward": { seconds: number } | undefined; - "seek-backward": { seconds: number } | und... + "seek-forward": { seconds: number } | undefined; + "seek-backward": { seconds: number } | undef... export type TActionWithArgs = keyof TActionArgsMap export type TActionWithOptionalArgs = | TActionWithNoArgs - | TKeysWithValueUndefined + | TKeysWithValueUndefined export type TActionWithNoArgs = Exclude export type TArgOfAction = A extends TActionWithArgs - ? TActionArgsMap[A] - : undefined + ? TActionArgsMap[A] + : undefined export type TActionFunc = A extends TActionWithArgs - ? (arg: TArgOfAction, trigger?: TInvocationTrigger) => void - : (_... + ? (arg: TArgOfAction, trigger?: TInvocationTrigger) => void + : (_?:... export type TInvocationTrigger = "keypress" | "mouseclick" export type TBoundActionList = { - [A in TAction]?: Array>; + [A in TAction]?: Array>; } export type TActionHandlerOptions = | MutableRefObject - | boolean - | undefined + | boolean + | undefined ## apps/web/src/lib/auth @@ -600,9 +604,9 @@ query.ts export function getCategories() export function getAuthors() export function processHtmlContent({ - html, + html, }: { - html: string; + html: string; }): Promise ## apps/web/src/lib/db @@ -620,31 +624,31 @@ schema.ts canvas.ts export function drawCssBackground({ - ctx, - width, - height, - css, + ctx, + width, + height, + css, }: { - ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; - width: number; - height: number; - css: string; + ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; + width: number; + height: number; + css: string; }): void parser.ts export type GradientOrientation = LinearOrientation | Array export type Color = | { type: "hex"; value: string } - | { type: "literal"; value: string } - | { type: "rgb"; value:... + | { type: "literal"; value: string } + | { type: "rgb"; value: A... export type ColorStop = Color & { length?: Distance } export type GradientAst = { - type: GradientType; - orientation: GradientOrientation | undefined; - colorStops: Array => ... export const GradientParser @@ -652,336 +656,337 @@ parser.ts audio.ts export type CollectedAudioElement = Omit< - AudioElement, - "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl... + AudioElement, + "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl" + ... export function createAudioContext(): AudioContext export interface DecodedAudio { samples: Float32Array sampleRate: number } export function decodeAudioToFloat32({ - audioBlob, + audioBlob, }: { - audioBlob: Blob; + audioBlob: Blob; }): Promise export function collectAudioElements({ - tracks, - mediaAssets, - audioContext, + tracks, + mediaAssets, + audioContext, }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - audioContext: AudioContext; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + audioContext: AudioContext; }): Promise export function collectAudioMixSources({ - tracks, - mediaAssets, + tracks, + mediaAssets, }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; }): Promise export function createTimelineAudioBuffer({ - tracks, - mediaAssets, - duration, - sampleRate = 44100, + tracks, + mediaAssets, + duration, + sampleRate = 44100, }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: number; - sampleRate?: number; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + duration: number; + sampleRate?: number; }): Promise media-utils.ts export const SUPPORTS_AUDIO: readonly MediaType[] export function mediaSupportsAudio({ - media, + media, }: { - media: MediaAsset | null | undefined; + media: MediaAsset | null | undefined; }): boolean export const getMediaTypeFromFile = ({ - file, + file, }: { - file: File; + file: File; }): MediaType | null => ... mediabunny.ts export function getVideoInfo({ - videoFile, + videoFile, }: { - videoFile: File; + videoFile: File; }): Promise<{ - duration: number; - width: number; - height: number; - fps: number; + duration: number; + width: number; + height: number; + fps: number; }> export const extractTimelineAudio = ({ - tracks, - mediaAssets, - totalDuration, - onProgress, + tracks, + mediaAssets, + totalDuration, + onProgress, }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - totalDuration: number; - onProgress?: (progress: number) => void; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + totalDuration: number; + onProgress?: (progress: number) => void; }): Promise => ... processing.ts export interface ProcessedMediaAsset extends Omit export function generateThumbnail({ - videoFile, - timeInSeconds, + videoFile, + timeInSeconds, }: { - videoFile: File; - timeInSeconds: number; + videoFile: File; + timeInSeconds: number; }): Promise export function processMediaAssets({ - files, - onProgress, + files, + onProgress, }: { - files: FileList | File[]; - onProgress?: ({ progress }: { progress: number }) => void; + files: FileList | File[]; + onProgress?: ({ progress }: { progress: number }) => void; }): Promise ## apps/web/src/lib/timeline bookmarks.ts export function findBookmarkIndex({ - bookmarks, - frameTime, + bookmarks, + frameTime, }: { - bookmarks: number[]; - frameTime: number; + bookmarks: number[]; + frameTime: number; }): number export function isBookmarkAtTime({ - bookmarks, - frameTime, + bookmarks, + frameTime, }: { - bookmarks: number[]; - frameTime: number; + bookmarks: number[]; + frameTime: number; }): boolean export function toggleBookmarkInArray({ - bookmarks, - frameTime, + bookmarks, + frameTime, }: { - bookmarks: number[]; - frameTime: number; + bookmarks: number[]; + frameTime: number; }): number[] export function removeBookmarkFromArray({ - bookmarks, - frameTime, + bookmarks, + frameTime, }: { - bookmarks: number[]; - frameTime: number; + bookmarks: number[]; + frameTime: number; }): number[] export function getFrameTime({ - time, - fps, + time, + fps, }: { - time: number; - fps: number; + time: number; + fps: number; }): number drop-utils.ts export function computeDropTarget({ - elementType, - mouseX, - mouseY, - tracks, - playheadTime, - isExternalDrop, - elementDuration, - pixelsPerSecond, - zoomLevel, - verticalDragDirection, - startTimeOverride, - excludeElementId, + elementType, + mouseX, + mouseY, + tracks, + playheadTime, + isExternalDrop, + elementDuration, + pixelsPerSecond, + zoomLevel, + verticalDragDirection, + startTimeOverride, + excludeElementId, }: ComputeDropTargetParams): DropTarget export function getDropLineY({ - dropTarget, - tracks, + dropTarget, + tracks, }: { - dropTarget: DropTarget; - tracks: TimelineTrack[]; + dropTarget: DropTarget; + tracks: TimelineTrack[]; }): number element-utils.ts export function canElementHaveAudio( - element: TimelineElement, + element: TimelineElement, ) export function canElementBeHidden( - element: TimelineElement, + element: TimelineElement, ) export function hasMediaId( - element: TimelineElement, + element: TimelineElement, ) export function requiresMediaId({ - element, + element, }: { - element: CreateTimelineElement; + element: CreateTimelineElement; }): boolean export function checkElementOverlaps({ - elements, + elements, }: { - elements: TimelineElement[]; + elements: TimelineElement[]; }): boolean export function resolveElementOverlaps({ - elements, + elements, }: { - elements: TimelineElement[]; + elements: TimelineElement[]; }): TimelineElement[] export function wouldElementOverlap({ - elements, - startTime, - endTime, - excludeElementId, + elements, + startTime, + endTime, + excludeElementId, }: { - elements: TimelineElement[]; - startTime: number; - endTime: number; - excludeElementId?: string; + elements: TimelineElement[]; + startTime: number; + endTime: number; + excludeElementId?: string; }): boolean export function buildTextElement({ - raw, - startTime, + raw, + startTime, }: { - raw: Partial>; - startTime: number; + raw: Partial>; + startTime: number; }): CreateTimelineElement export function buildStickerElement({ - iconName, - startTime, + iconName, + startTime, }: { - iconName: string; - startTime: number; + iconName: string; + startTime: number; }): CreateStickerElement export function buildUploadAudioElement({ - mediaId, - name, - duration, - startTime, - buffer, + mediaId, + name, + duration, + startTime, + buffer, }: { - mediaId: string; - name: string; - duration: number; - startTime: number; - buffer?: AudioBuffer; + mediaId: string; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; }): CreateUploadAudioElement export function buildLibraryAudioElement({ - sourceUrl, - name, - duration, - startTime, - buffer, + sourceUrl, + name, + duration, + startTime, + buffer, }: { - sourceUrl: string; - name: string; - duration: number; - startTime: number; - buffer?: AudioBuffer; + sourceUrl: string; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; }): CreateLibraryAudioElement index.ts export function calculateTotalDuration({ - tracks, + tracks, }: { - tracks: TimelineTrack[]; + tracks: TimelineTrack[]; }): number track-utils.ts export function canTracktHaveAudio( - track: TimelineTrack, + track: TimelineTrack, ) export function canTrackBeHidden( - track: TimelineTrack, + track: TimelineTrack, ) export function getTrackColor({ type }: { type: TrackType }) export function getTrackClasses({ type }: { type: TrackType }) export function getTrackHeight({ type }: { type: TrackType }): number export function getCumulativeHeightBefore({ - tracks, - trackIndex, + tracks, + trackIndex, }: { - tracks: Array<{ type: TrackType }>; - trackIndex: number; + tracks: Array<{ type: TrackType }>; + trackIndex: number; }): number export function getTotalTracksHeight({ - tracks, + tracks, }: { - tracks: Array<{ type: TrackType }>; + tracks: Array<{ type: TrackType }>; }): number export function buildEmptyTrack({ - id, - type, - name, + id, + type, + name, }: { - id: string; - type: TrackType; - name?: string; + id: string; + type: TrackType; + name?: string; }): TimelineTrack export function getDefaultInsertIndexForTrack({ - tracks, - trackType, + tracks, + trackType, }: { - tracks: TimelineTrack[]; - trackType: TrackType; + tracks: TimelineTrack[]; + trackType: TrackType; }): number export function getHighestInsertIndexForTrack({ - tracks, - trackType, + tracks, + trackType, }: { - tracks: TimelineTrack[]; - trackType: TrackType; + tracks: TimelineTrack[]; + trackType: TrackType; }): number export function isMainTrack(track: TimelineTrack) export function getMainTrack({ - tracks, + tracks, }: { - tracks: TimelineTrack[]; + tracks: TimelineTrack[]; }): TimelineTrack | null export function ensureMainTrack({ - tracks, + tracks, }: { - tracks: TimelineTrack[]; + tracks: TimelineTrack[]; }): TimelineTrack[] export function canElementGoOnTrack({ - elementType, - trackType, + elementType, + trackType, }: { - elementType: ElementType; - trackType: TrackType; + elementType: ElementType; + trackType: TrackType; }): boolean export function validateElementTrackCompatibility({ - element, - track, + element, + track, }: { - element: { type: ElementType }; - track: { type: TrackType }; + element: { type: ElementType }; + track: { type: TrackType }; }): { isValid: boolean; errorMessage?: string } zoom-utils.ts export function getTimelineZoomMin({ - duration, - containerWidth, + duration, + containerWidth, }: { - duration: number; - containerWidth: number | null | undefined; + duration: number; + containerWidth: number | null | undefined; }): number ## apps/web/src/lib/transcription caption.ts export function buildCaptionChunks({ - segments, - wordsPerChunk = DEFAULT_WORDS_PER_CAPTION, - minDuration = MIN_CAPTION_DURATION_SECONDS, + segments, + wordsPerChunk = DEFAULT_WORDS_PER_CAPTION, + minDuration = MIN_CAPTION_DURATION_SECONDS, }: { - segments: TranscriptionSegment[]; - wordsPerChunk?: number; - minDuration?: number; + segments: TranscriptionSegment[]; + wordsPerChunk?: number; + minDuration?: number; }): CaptionChunk[] ## apps/web/src/services/media @@ -991,14 +996,14 @@ video-cache.ts sinks initPromises async getFrameAt({ - mediaId, - file, - time, - }: { - mediaId: string; - file: File; - time: number; - }): Promise + mediaId, + file, + time, + }: { + mediaId: string; + file: File; + time: number; + }): Promise clearVideo({ mediaId }: { mediaId: string }): void clearAll(): void getStats() @@ -1009,9 +1014,9 @@ video-cache.ts canvas-renderer.ts export type CanvasRendererParams = { - width: number; - height: number; - fps: number; + width: number; + height: number; + fps: number; } export class CanvasRenderer { canvas: OffscreenCanvas | HTMLCanvasElement @@ -1023,32 +1028,32 @@ canvas-renderer.ts setSize({ width, height }: { width: number; height: number }) async render({ node, time }: { node: BaseNode; time: number }) async renderToCanvas({ - node, - time, - targetCanvas, - }: { - node: BaseNode; - time: number; - targetCanvas: HTMLCanvasElement; - }) + node, + time, + targetCanvas, + }: { + node: BaseNode; + time: number; + targetCanvas: HTMLCanvasElement; + }) } scene-builder.ts export type BuildSceneParams = { - canvasSize: TCanvasSize; - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: ... + canvasSize: TCanvasSize; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + duration: numb... export function buildScene(params: BuildSceneParams) scene-exporter.ts export type ExportFormat = "mp4" | "webm" export type ExportQuality = "low" | "medium" | "high" | "very_high" export type SceneExporterEvents = { - progress: [progress: number]; - complete: [buffer: ArrayBuffer]; - error: [error: Error]; - c... + progress: [progress: number]; + complete: [buffer: ArrayBuffer]; + error: [error: Error]; + cance... export class SceneExporter extends EventEmitter { renderer: CanvasRenderer format: ExportFormat @@ -1115,18 +1120,19 @@ types.ts sourceStickerIconName?: string } export type SerializedScene = Omit & { - createdAt: string; - updatedAt: string; + createdAt: string; + updatedAt: string; } export type SerializedProjectMetadata = Omit< - TProjectMetadata, - "createdAt" | "updatedAt" + TProjectMetadata, + "createdAt" | "updatedAt" > & { - createdAt: string; - updatedAt: str... + createdAt: string; + updatedAt: string; + } export type SerializedProject = Omit & { - metadata: SerializedProjectMetadata; - scenes: Serial... + metadata: SerializedProjectMetadata; + scenes: Serializ... export interface StorageConfig { projectsDb: string mediaDb: string @@ -1141,10 +1147,10 @@ index.ts worker.ts export type WorkerMessage = | { type: "init"; modelId: string } - | { type: "transcribe"; audio: Float32Array; language: stri... + | { type: "transcribe"; audio: Float32Array; language: strin... export type WorkerResponse = | { type: "init-progress"; progress: number } - | { type: "init-complete" } - | { type: "init-err... + | { type: "init-complete" } + | { type: "init-error... ## apps/web/src/stores @@ -1200,70 +1206,67 @@ timeline-store.ts assets.ts export type MediaType = "image" | "video" | "audio" - export interface MediaAsset extends Omit< - MediaAssetData, - "size" | "lastModified" - > { + export interface MediaAsset extends Omit { file: File url?: string } blog.ts export type Post = { - id: string; - slug: string; - title: string; - content: string; - description: string; - cove... + id: string; + slug: string; + title: string; + content: string; + description: string; + coverImage... export type Pagination = { - limit: number; - currpage: number; - nextPage: number | null; - prevPage: number | null; - to... + limit: number; + currpage: number; + nextPage: number | null; + prevPage: number | null; + totalIt... export type MarblePostList = { - posts: Post[]; - pagination: Pagination; + posts: Post[]; + pagination: Pagination; } export type MarblePost = { - post: Post; + post: Post; } export type Tag = { - id: string; - name: string; - slug: string; + id: string; + name: string; + slug: string; } export type MarbleTag = { - tag: Tag; + tag: Tag; } export type MarbleTagList = { - tags: Tag[]; - pagination: Pagination; + tags: Tag[]; + pagination: Pagination; } export type Category = { - id: string; - name: string; - slug: string; + id: string; + name: string; + slug: string; } export type MarbleCategory = { - category: Category; + category: Category; } export type MarbleCategoryList = { - categories: Category[]; - pagination: Pagination; + categories: Category[]; + pagination: Pagination; } export type Author = { - id: string; - name: string; - image: string; + id: string; + name: string; + image: string; } export type MarbleAuthor = { - author: Author; + author: Author; } export type MarbleAuthorList = { - authors: Author[]; - pagination: Pagination; + authors: Author[]; + pagination: Pagination; } drag.ts @@ -1306,40 +1309,47 @@ export.ts keybinding.ts export type ModifierKeys = | "ctrl" - | "alt" - | "shift" - | "ctrl+shift" - | "alt+shift" - | "ctrl+alt" - | "ctrl+alt+shift" + | "alt" + | "shift" + | "ctrl+shift" + | "alt+shift" + | "ctrl+alt" + | "ctrl+alt+shift" export type Key = | "a" - | "b" - | "c" - | "d" - | "e" - | "f" - | "g" - | "h" - | "i" - | "j" - | "k" - | "l" - |... + | "b" + | "c" + | "d" + | "e" + | "f" + | "g" + | "h" + | "i" + | "j" + | "k" + | "l" + | "m" + | "n" + ... export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}` export type SingleCharacterShortcutKey = `${Key}` export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey export type KeybindingConfig = { - [key in ShortcutKey]?: TActionWithOptionalArgs; + [key in ShortcutKey]?: TActionWithOptionalArgs; } +language.ts + export type Language = (typeof LANGUAGES)[number] + export type LanguageCode = Language["code"] + project.ts export type TBackground = | { - type: "color"; - color: string; - } - | { - type: "blur"; - blurIntensity... + type: "color"; + color: string; + } + | { + type: "blur"; + blurIntensity: number; + } export interface TCanvasSize { width: number height: number @@ -1447,9 +1457,9 @@ timeline.ts export interface Transform { scale: number position: { - x: number; - y: number; - } + x: number; + y: number; + } rotate: number } export interface UploadAudioElement extends BaseAudioElement { @@ -1500,24 +1510,24 @@ timeline.ts color?: string } export type TimelineElement = | AudioElement - | VideoElement - | ImageElement - | TextElement - | StickerElement + | VideoElement + | ImageElement + | TextElement + | StickerElement export type ElementType = TimelineElement["type"] export type CreateUploadAudioElement = Omit export type CreateLibraryAudioElement = Omit export type CreateAudioElement = | CreateUploadAudioElement - | CreateLibraryAudioElement + | CreateLibraryAudioElement export type CreateVideoElement = Omit export type CreateImageElement = Omit export type CreateTextElement = Omit export type CreateStickerElement = Omit export type CreateTimelineElement = | CreateAudioElement - | CreateVideoElement - | CreateImageElement - | CreateTextElement - | Crea... + | CreateVideoElement + | CreateImageElement + | CreateTextElement + | CreateSt... export interface ElementDragState { isDragging: boolean elementId: string | null @@ -1556,6 +1566,7 @@ timeline.ts } transcription.ts + export type TranscriptionLanguage = LanguageCode | "auto" export interface TranscriptionSegment { text: string start: number @@ -1567,19 +1578,19 @@ transcription.ts language: string } export type TranscriptionStatus = | "idle" - | "loading-model" - | "transcribing" - | "complete" - | "error" + | "loading-model" + | "transcribing" + | "complete" + | "error" export interface TranscriptionProgress { status: TranscriptionStatus progress: number message?: string } export type TranscriptionModelId = | "whisper-tiny" - | "whisper-small" - | "whisper-medium" - | "whisper-large-v3-turbo" + | "whisper-small" + | "whisper-medium" + | "whisper-large-v3-turbo" export interface TranscriptionModel { id: TranscriptionModelId name: string @@ -1596,9 +1607,9 @@ transcription.ts browser.ts export function isTypableDOMElement({ - element, + element, }: { - element: HTMLElement; + element: HTMLElement; }): boolean date.ts @@ -1606,11 +1617,11 @@ date.ts geometry.ts export function dimensionToAspectRatio({ - width, - height, + width, + height, }: { - width: number; - height: number; + width: number; + height: number; }): string id.ts @@ -1618,13 +1629,13 @@ id.ts math.ts export function clamp({ - value, - min, - max, + value, + min, + max, }: { - value: number; - min: number; - max: number; + value: number; + min: number; + max: number; }): number platform.ts @@ -1641,46 +1652,72 @@ ui.ts ## packages/ui/src/icons +base-icon.tsx + export function BaseIcon({ + title, + size = 24, + viewBox = "0 0 24 24", + children, + ...props + }: BaseIconProps) + +create-icon.tsx + export function createIcon({ definition }: CreateIconParams) + index.tsx - export function GoogleIcon({ className }: { className?: string }) - export function GithubIcon({ className }: { className?: string }) - export function VercelIcon({ className }: { className?: string }) - export function BackgroundIcon({ className }: { className?: string }) - export function MarbleIcon({ - className = "", - size = 32, - }: { - className?: string; - size?: number; - }) - export function DataBuddyIcon({ - className = "", - size = 32, - }: { - className?: string; - size?: number; - }) - export function SocialsIcon({ - className = "", - size = 32, - }: { - className?: string; - size?: number; - }) - export function TransitionUpIcon({ - className = "", - size = 16, - }: { - className?: string; - size?: number; - }) - export function MenuIcon({ - className = "", - size = 16, - }: { - className?: string; - size?: number; - }) + export const GoogleIcon + export const GithubIcon + export const VercelIcon + export const DataBuddyIcon + export const MarbleIcon + export const BackgroundIcon + export const SocialsIcon + export const TransitionUpIcon + export const MenuIcon + export const PencilIcon + export const LeftArrowIcon + export const TrashIcon + +types.ts + export type IconProps = Omit< + SVGProps, + "children" | "width" | "height" + > & { + size?: number; + title?: s... + export type IconNode = Array<{ + element: keyof JSX.IntrinsicElements; + props: Record, + result: z.infer, ) { - return { - id: result.id, - name: result.name, - description: result.description, - url: result.url, - previewUrl: - result.previews?.["preview-hq-mp3"] || - result.previews?.["preview-lq-mp3"], - downloadUrl: result.download, - duration: result.duration, - filesize: result.filesize, - type: result.type, - channels: result.channels, - bitrate: result.bitrate, - bitdepth: result.bitdepth, - samplerate: result.samplerate, - username: result.username, - tags: result.tags, - license: result.license, - created: result.created, - downloads: result.num_downloads || 0, - rating: result.avg_rating || 0, - ratingCount: result.num_ratings || 0, - }; + return { + id: result.id, + name: result.name, + description: result.description, + url: result.url, + previewUrl: + result.previews?.["preview-hq-mp3"] || + result.previews?.["preview-lq-mp3"], + downloadUrl: result.download, + duration: result.duration, + filesize: result.filesize, + type: result.type, + channels: result.channels, + bitrate: result.bitrate, + bitdepth: result.bitdepth, + samplerate: result.samplerate, + username: result.username, + tags: result.tags, + license: result.license, + created: result.created, + downloads: result.num_downloads || 0, + rating: result.avg_rating || 0, + ratingCount: result.num_ratings || 0, + }; } export async function GET(request: NextRequest) { - try { - const { limited } = await checkRateLimit({ request }); - if (limited) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } + try { + const { limited } = await checkRateLimit({ request }); + if (limited) { + return NextResponse.json({ error: "Too many requests" }, { status: 429 }); + } - const { searchParams } = new URL(request.url); + const { searchParams } = new URL(request.url); - const validationResult = searchParamsSchema.safeParse({ - q: searchParams.get("q") || undefined, - type: searchParams.get("type") || undefined, - page: searchParams.get("page") || undefined, - page_size: searchParams.get("page_size") || undefined, - sort: searchParams.get("sort") || undefined, - min_rating: searchParams.get("min_rating") || undefined, - }); + const validationResult = searchParamsSchema.safeParse({ + q: searchParams.get("q") || undefined, + type: searchParams.get("type") || undefined, + page: searchParams.get("page") || undefined, + page_size: searchParams.get("page_size") || undefined, + sort: searchParams.get("sort") || undefined, + min_rating: searchParams.get("min_rating") || undefined, + }); - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 }, - ); - } + if (!validationResult.success) { + return NextResponse.json( + { + error: "Invalid parameters", + details: validationResult.error.flatten().fieldErrors, + }, + { status: 400 }, + ); + } - const { - q: query, - type, - page, - page_size: pageSize, - sort, - min_rating, - commercial_only, - } = validationResult.data; + const { + q: query, + type, + page, + page_size: pageSize, + sort, + min_rating, + commercial_only, + } = validationResult.data; - if (type === "songs") { - return NextResponse.json( - { - error: "Songs are not available yet", - message: - "Song search functionality is coming soon. Try searching for sound effects instead.", - }, - { status: 501 }, - ); - } + if (type === "songs") { + return NextResponse.json( + { + error: "Songs are not available yet", + message: + "Song search functionality is coming soon. Try searching for sound effects instead.", + }, + { status: 501 }, + ); + } - const baseUrl = "https://freesound.org/apiv2/search/text/"; + const baseUrl = "https://freesound.org/apiv2/search/text/"; - const sortParam = buildSortParameter({ query, sort }); + const sortParam = buildSortParameter({ query, sort }); - const params = new URLSearchParams({ - query: query || "", - token: webEnv.FREESOUND_API_KEY, - page: page.toString(), - page_size: pageSize.toString(), - sort: sortParam, - fields: - "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", - }); + const params = new URLSearchParams({ + query: query || "", + token: webEnv.FREESOUND_API_KEY, + page: page.toString(), + page_size: pageSize.toString(), + sort: sortParam, + fields: + "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", + }); - const isEffectsSearch = type === "effects" || !type; - if (isEffectsSearch) { - applyEffectsFilters({ params, min_rating, commercial_only }); - } + const isEffectsSearch = type === "effects" || !type; + if (isEffectsSearch) { + applyEffectsFilters({ params, min_rating, commercial_only }); + } - const response = await fetch(`${baseUrl}?${params.toString()}`); + const response = await fetch(`${baseUrl}?${params.toString()}`); - if (!response.ok) { - const errorText = await response.text(); - console.error("Freesound API error:", response.status, errorText); - return NextResponse.json( - { error: "Failed to search sounds" }, - { status: response.status }, - ); - } + if (!response.ok) { + const errorText = await response.text(); + console.error("Freesound API error:", response.status, errorText); + return NextResponse.json( + { error: "Failed to search sounds" }, + { status: response.status }, + ); + } - const rawData = await response.json(); + const rawData = await response.json(); - const freesoundValidation = freesoundResponseSchema.safeParse(rawData); - if (!freesoundValidation.success) { - console.error( - "Invalid Freesound API response:", - freesoundValidation.error, - ); - return NextResponse.json( - { error: "Invalid response from Freesound API" }, - { status: 502 }, - ); - } + const freesoundValidation = freesoundResponseSchema.safeParse(rawData); + if (!freesoundValidation.success) { + console.error( + "Invalid Freesound API response:", + freesoundValidation.error, + ); + return NextResponse.json( + { error: "Invalid response from Freesound API" }, + { status: 502 }, + ); + } - const data = freesoundValidation.data; + const data = freesoundValidation.data; - const transformedResults = data.results.map(transformFreesoundResult); + const transformedResults = data.results.map(transformFreesoundResult); - const responseData = { - count: data.count, - next: data.next, - previous: data.previous, - results: transformedResults, - query: query || "", - type: type || "effects", - page, - pageSize, - sort, - minRating: min_rating, - }; + const responseData = { + count: data.count, + next: data.next, + previous: data.previous, + results: transformedResults, + query: query || "", + type: type || "effects", + page, + pageSize, + sort, + minRating: min_rating, + }; - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error, - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 }, - ); - } + const responseValidation = apiResponseSchema.safeParse(responseData); + if (!responseValidation.success) { + console.error( + "Invalid API response structure:", + responseValidation.error, + ); + return NextResponse.json( + { error: "Internal response formatting error" }, + { status: 500 }, + ); + } - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Error searching sounds:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } + return NextResponse.json(responseValidation.data); + } catch (error) { + console.error("Error searching sounds:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } } diff --git a/apps/web/src/app/base-page.tsx b/apps/web/src/app/base-page.tsx index 38eca161..a3d56fe5 100644 --- a/apps/web/src/app/base-page.tsx +++ b/apps/web/src/app/base-page.tsx @@ -3,51 +3,51 @@ import { Footer } from "@/components/footer"; import { cn } from "@/utils/ui"; interface BasePageProps { - children: React.ReactNode; - className?: string; - mainClassName?: string; - maxWidth?: "3xl" | "6xl" | "full"; - title?: string; - description?: string; + children: React.ReactNode; + className?: string; + mainClassName?: string; + maxWidth?: "3xl" | "6xl" | "full"; + title?: string; + description?: string; } export function BasePage({ - children, - className = "", - mainClassName = "", - maxWidth = "3xl", - title, - description, + children, + className = "", + mainClassName = "", + maxWidth = "3xl", + title, + description, }: BasePageProps) { - const maxWidthClass = { - "3xl": "max-w-3xl", - "6xl": "max-w-6xl", - full: "max-w-full", - }[maxWidth]; + const maxWidthClass = { + "3xl": "max-w-3xl", + "6xl": "max-w-6xl", + full: "max-w-full", + }[maxWidth]; - return ( -
-
-
- {title && description && ( -
-

- {title} -

-

- {description} -

-
- )} - {children} -
-
-
- ); + return ( +
+
+
+ {title && description && ( +
+

+ {title} +

+

+ {description} +

+
+ )} + {children} +
+
+
+ ); } diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index 7325d859..46c6163d 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -1,152 +1,152 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import { notFound } from "next/navigation"; import { BasePage } from "@/app/base-page"; import Prose from "@/components/ui/prose"; import { Separator } from "@/components/ui/separator"; import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query"; -import { Post, Author } from "@/types/blog"; -import { Metadata } from "next"; -import Image from "next/image"; -import { notFound } from "next/navigation"; +import type { Author, Post } from "@/types/blog"; type PageProps = { - params: Promise<{ slug: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + params: Promise<{ slug: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }; export async function generateMetadata({ - params, + params, }: PageProps): Promise { - const slug = (await params).slug; + const slug = (await params).slug; - const data = await getSinglePost({ slug }); + const data = await getSinglePost({ slug }); - if (!data || !data.post) return {}; + if (!data || !data.post) return {}; - return { - title: data.post.title, - description: data.post.description, - twitter: { - title: `${data.post.title}`, - description: `${data.post.description}`, - card: "summary_large_image", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - }, - openGraph: { - type: "article", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - title: data.post.title, - description: data.post.description, - publishedTime: new Date(data.post.publishedAt).toISOString(), - authors: data.post.authors.map((author: Author) => author.name), - }, - }; + return { + title: data.post.title, + description: data.post.description, + twitter: { + title: `${data.post.title}`, + description: `${data.post.description}`, + card: "summary_large_image", + images: [ + { + url: data.post.coverImage, + width: "1200", + height: "630", + alt: data.post.title, + }, + ], + }, + openGraph: { + type: "article", + images: [ + { + url: data.post.coverImage, + width: "1200", + height: "630", + alt: data.post.title, + }, + ], + title: data.post.title, + description: data.post.description, + publishedTime: new Date(data.post.publishedAt).toISOString(), + authors: data.post.authors.map((author: Author) => author.name), + }, + }; } export async function generateStaticParams() { - const data = await getPosts(); - if (!data || !data.posts.length) return []; + const data = await getPosts(); + if (!data || !data.posts.length) return []; - return data.posts.map((post) => ({ - slug: post.slug, - })); + return data.posts.map((post) => ({ + slug: post.slug, + })); } export default async function BlogPostPage({ params }: PageProps) { - const slug = (await params).slug; - const data = await getSinglePost({ slug }); - if (!data || !data.post) return notFound(); + const slug = (await params).slug; + const data = await getSinglePost({ slug }); + if (!data || !data.post) return notFound(); - const html = await processHtmlContent({ html: data.post.content }); + const html = await processHtmlContent({ html: data.post.content }); - return ( - - - - - - ); + return ( + + + + + + ); } function PostHeader({ post }: { post: Post }) { - const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", { - day: "numeric", - month: "long", - year: "numeric", - }); + const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", { + day: "numeric", + month: "long", + year: "numeric", + }); - return ( - <> - {post.coverImage && } - - - - - ); + return ( + <> + {post.coverImage && } + + + + + ); } function PostCoverImage({ post }: { post: Post }) { - return ( -
- {post.title} -
- ); + return ( +
+ {post.title} +
+ ); } function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) { - return ( -
- -
- ); + return ( +
+ +
+ ); } function PostTitle({ title }: { title: string }) { - return ( -

{title}

- ); + return ( +

{title}

+ ); } function PostAuthor({ author }: { author?: Author }) { - if (!author) return null; + if (!author) return null; - return ( -
- {author.name} -

{author.name}

-
- ); + return ( +
+ {author.name} +

{author.name}

+
+ ); } function PostContent({ html }: { html: string }) { - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx index 050f59ae..64a539d0 100644 --- a/apps/web/src/app/blog/page.tsx +++ b/apps/web/src/app/blog/page.tsx @@ -1,73 +1,73 @@ -import { Metadata } from "next"; +import type { Metadata } from "next"; +import Link from "next/link"; import { BasePage } from "@/app/base-page"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import Link from "next/link"; -import { getPosts } from "@/lib/blog/query"; -import { Post, Author } from "@/types/blog"; import { Separator } from "@/components/ui/separator"; +import { getPosts } from "@/lib/blog/query"; +import type { Author, Post } from "@/types/blog"; export const metadata: Metadata = { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - openGraph: { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - type: "website", - }, + title: "Blog - OpenCut", + description: + "Read the latest news and updates about OpenCut, the free and open-source video editor.", + openGraph: { + title: "Blog - OpenCut", + description: + "Read the latest news and updates about OpenCut, the free and open-source video editor.", + type: "website", + }, }; export default async function BlogPage() { - const data = await getPosts(); - if (!data || !data.posts) return
No posts yet
; + const data = await getPosts(); + if (!data || !data.posts) return
No posts yet
; - return ( - -
- {data.posts.map((post) => ( -
- - -
- ))} -
-
- ); + return ( + +
+ {data.posts.map((post) => ( +
+ + +
+ ))} +
+
+ ); } function BlogPostItem({ post }: { post: Post }) { - return ( - -
-
-

{post.title}

-

{post.description}

-
- {post.authors && post.authors.length > 0 && ( - - )} -
- - ); + return ( + +
+
+

{post.title}

+

{post.description}

+
+ {post.authors && post.authors.length > 0 && ( + + )} +
+ + ); } function AuthorList({ authors }: { authors: Author[] }) { - return ( -
- {authors.map((author) => ( -
- - - - {author.name.charAt(0).toUpperCase()} - - -
- ))} -
- ); + return ( +
+ {authors.map((author) => ( +
+ + + + {author.name.charAt(0).toUpperCase()} + + +
+ ))} +
+ ); } diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx index 716002fa..fd159576 100644 --- a/apps/web/src/app/contributors/page.tsx +++ b/apps/web/src/app/contributors/page.tsx @@ -1,278 +1,278 @@ -import { Metadata } from "next"; -import { Card, CardContent } from "@/components/ui/card"; +import { GithubIcon } from "@opencut/ui/icons"; +import { ExternalLink } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { GitHubContributeSection } from "@/components/gitHub-contribute-section"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; -import { ExternalLink } from "lucide-react"; -import Link from "next/link"; -import { GithubIcon } from "@opencut/ui/icons"; +import { Card, CardContent } from "@/components/ui/card"; import { EXTERNAL_TOOLS, SOCIAL_LINKS } from "@/constants/site-constants"; -import { GitHubContributeSection } from "@/components/gitHub-contribute-section"; import { BasePage } from "../base-page"; export const metadata: Metadata = { - title: "Contributors - OpenCut", - description: - "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", - openGraph: { - title: "Contributors - OpenCut", - description: - "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", - type: "website", - }, + title: "Contributors - OpenCut", + description: + "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", + openGraph: { + title: "Contributors - OpenCut", + description: + "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", + type: "website", + }, }; interface Contributor { - id: number; - login: string; - avatar_url: string; - html_url: string; - contributions: number; - type: string; + id: number; + login: string; + avatar_url: string; + html_url: string; + contributions: number; + type: string; } async function getContributors(): Promise { - try { - const response = await fetch( - "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100", - { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "OpenCut-Web-App", - }, - next: { revalidate: 600 }, // 10 minutes - }, - ); + try { + const response = await fetch( + "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100", + { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCut-Web-App", + }, + next: { revalidate: 600 }, // 10 minutes + }, + ); - if (!response.ok) { - console.error("Failed to fetch contributors"); - return []; - } + if (!response.ok) { + console.error("Failed to fetch contributors"); + return []; + } - const contributors = (await response.json()) as Contributor[]; + const contributors = (await response.json()) as Contributor[]; - const filteredContributors = contributors.filter( - (contributor) => contributor.type === "User", - ); + const filteredContributors = contributors.filter( + (contributor) => contributor.type === "User", + ); - return filteredContributors; - } catch (error) { - console.error("Error fetching contributors:", error); - return []; - } + return filteredContributors; + } catch (error) { + console.error("Error fetching contributors:", error); + return []; + } } export default async function ContributorsPage() { - const contributors = await getContributors(); - const topContributors = contributors.slice(0, 2); - const otherContributors = contributors.slice(2); - const totalContributions = contributors.reduce( - (sum, c) => sum + c.contributions, - 0, - ); + const contributors = await getContributors(); + const topContributors = contributors.slice(0, 2); + const otherContributors = contributors.slice(2); + const totalContributions = contributors.reduce( + (sum, c) => sum + c.contributions, + 0, + ); - return ( - -
- - -
+ return ( + +
+ + +
-
- {topContributors.length > 0 && ( - - )} - {otherContributors.length > 0 && ( - - )} - {contributors.length === 0 && } - - -
-
- ); +
+ {topContributors.length > 0 && ( + + )} + {otherContributors.length > 0 && ( + + )} + {contributors.length === 0 && } + + +
+
+ ); } function StatItem({ value, label }: { value: number; label: string }) { - return ( -
-
- {value} - {label} -
- ); + return ( +
+
+ {value} + {label} +
+ ); } function TopContributorsSection({ - contributors, + contributors, }: { - contributors: Contributor[]; + contributors: Contributor[]; }) { - return ( -
-
-

Top contributors

-

- Leading the way in contributions -

-
+ return ( +
+
+

Top contributors

+

+ Leading the way in contributions +

+
-
- {contributors.map((contributor) => ( - - ))} -
-
- ); +
+ {contributors.map((contributor) => ( + + ))} +
+
+ ); } function TopContributorCard({ contributor }: { contributor: Contributor }) { - return ( - - - - - - - {contributor.login.charAt(0).toUpperCase()} - - -
-

{contributor.login}

-
- {contributor.contributions} - contributions -
-
-
-
- - ); + return ( + + + + + + + {contributor.login.charAt(0).toUpperCase()} + + +
+

{contributor.login}

+
+ {contributor.contributions} + contributions +
+
+
+
+ + ); } function AllContributorsSection({ - contributors, + contributors, }: { - contributors: Contributor[]; + contributors: Contributor[]; }) { - return ( -
-
-

All contributors

-

- Everyone who makes OpenCut better -

-
+ return ( +
+
+

All contributors

+

+ Everyone who makes OpenCut better +

+
-
- {contributors.map((contributor, index) => ( - -
- - - - {contributor.login.charAt(0).toUpperCase()} - - -
-

{contributor.login}

-

- {contributor.contributions} -

-
-
- - ))} -
-
- ); +
+ {contributors.map((contributor) => ( + +
+ + + + {contributor.login.charAt(0).toUpperCase()} + + +
+

{contributor.login}

+

+ {contributor.contributions} +

+
+
+ + ))} +
+
+ ); } function EmptyState() { - return ( -
-
-
- -
-
-

No contributors found

-

- Unable to load contributors at the moment. Check back later or view - on GitHub. -

-
-
- - - -
- ); + return ( +
+
+
+ +
+
+

No contributors found

+

+ Unable to load contributors at the moment. Check back later or view + on GitHub. +

+
+
+ + + +
+ ); } function ExternalToolsSection() { - return ( -
-
-

External tools

-

Tools we use to build OpenCut

-
+ return ( +
+
+

External tools

+

Tools we use to build OpenCut

+
-
- {EXTERNAL_TOOLS.map((tool, index) => ( - - - -
- -
-
-

{tool.name}

-

- {tool.description} -

-
-
-
- - ))} -
-
- ); +
+ {EXTERNAL_TOOLS.map((tool, index) => ( + + + +
+ +
+
+

{tool.name}

+

+ {tool.description} +

+
+
+
+ + ))} +
+
+ ); } diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index ac013fbe..958b0565 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -2,9 +2,9 @@ import { useParams } from "next/navigation"; import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, } from "@/components/ui/resizable"; import { AssetsPanel } from "@/components/editor/assets-panel"; import { PropertiesPanel } from "@/components/editor/properties-panel"; @@ -17,92 +17,92 @@ import { MigrationDialog } from "@/components/editor/migration-dialog"; import { usePanelStore } from "@/stores/panel-store"; export default function Editor() { - const params = useParams(); - const projectId = params.project_id as string; + const params = useParams(); + const projectId = params.project_id as string; - return ( - -
- -
- -
- - -
-
- ); + return ( + +
+ +
+ +
+ + +
+
+ ); } function EditorLayout() { - const { panels, setPanel } = usePanelStore(); + const { panels, setPanel } = usePanelStore(); - return ( - { - setPanel("mainContent", sizes[0] ?? panels.mainContent); - setPanel("timeline", sizes[1] ?? panels.timeline); - }} - > - - { - setPanel("tools", sizes[0] ?? panels.tools); - setPanel("preview", sizes[1] ?? panels.preview); - setPanel("properties", sizes[2] ?? panels.properties); - }} - > - - - + return ( + { + setPanel("mainContent", sizes[0] ?? panels.mainContent); + setPanel("timeline", sizes[1] ?? panels.timeline); + }} + > + + { + setPanel("tools", sizes[0] ?? panels.tools); + setPanel("preview", sizes[1] ?? panels.preview); + setPanel("properties", sizes[2] ?? panels.properties); + }} + > + + + - + - - - + + + - + - - - - - + + + + + - + - - - - - ); + + + + + ); } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 759b400c..9b547211 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -8,86 +8,89 @@ @plugin "tailwindcss-animate"; :root { - --background: hsl(0, 0%, 100%); - --foreground: hsl(0 0% 11%); - --card: hsl(0, 0%, 100%); - --card-foreground: hsl(0 0% 11%); - --popover: hsl(0, 0%, 100%); - --popover-foreground: hsl(0 0% 2%); - --primary: hsl(205, 84%, 47%); - --primary-foreground: hsl(0 0% 91%); - --secondary: hsl(216, 13%, 92%); - --secondary-foreground: hsl(0 0% 2%); - --muted: hsl(0 0% 85.1%); - --muted-foreground: hsl(0 0% 50%); - --accent: hsl(216, 13%, 92%); - --accent-foreground: hsl(0 0% 2%); - --destructive: hsl(0, 83%, 50%); - --destructive-foreground: hsl(0, 0%, 100%); - --constructive: hsl(141, 71%, 48%); - --constructive-foreground: hsl(0, 0%, 100%); - --border: hsl(0 0% 83%); - --input: hsl(0 0% 85.1%); - --ring: hsl(0, 0%, 55%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 96.1%); - --sidebar-foreground: hsl(0 0% 2%); - --sidebar-primary: hsl(0 0% 2%); - --sidebar-primary-foreground: hsl(0 0% 91%); - --sidebar-accent: hsl(0 0% 85.1%); - --sidebar-accent-foreground: hsl(0 0% 2%); - --sidebar-border: hsl(0 0% 85.1%); - --sidebar-ring: hsl(0 0% 16.9%); - --panel-background: hsl(216 13% 92%); - --panel-accent: hsl(216, 8%, 86%); + --background: hsl(0, 0%, 100%); + --foreground: hsl(0 0% 11%); + --card: hsl(0, 0%, 100%); + --card-foreground: hsl(0 0% 11%); + --popover: hsl(0, 0%, 100%); + --popover-foreground: hsl(0 0% 2%); + --primary: hsl(210, 91%, 49%); + --primary-foreground: hsl(0 0% 91%); + --secondary: hsl(216, 13%, 92%); + --secondary-foreground: hsl(0 0% 2%); + --muted: hsl(0 0% 85.1%); + --muted-foreground: hsl(0 0% 50%); + --accent: hsl(216, 13%, 92%); + --accent-foreground: hsl(0 0% 2%); + --destructive: hsl(0, 83%, 50%); + --destructive-foreground: hsl(0, 0%, 100%); + --constructive: hsl(141, 71%, 48%); + --constructive-foreground: hsl(0, 0%, 100%); + --border: hsl(0 0% 83%); + --input: hsl(0 0% 85.1%); + --ring: hsl(0, 0%, 55%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 96.1%); + --sidebar-foreground: hsl(0 0% 2%); + --sidebar-primary: hsl(0 0% 2%); + --sidebar-primary-foreground: hsl(0 0% 91%); + --sidebar-accent: hsl(0 0% 85.1%); + --sidebar-accent-foreground: hsl(0 0% 2%); + --sidebar-border: hsl(0 0% 85.1%); + --sidebar-ring: hsl(0 0% 16.9%); + --panel-background: hsl(216 13% 92%); + --panel-accent: hsl(216, 8%, 86%); - --radius: 1rem; + --radius: 1rem; + + --sidebar: hsl(0 0% 98%); } .dark { - --background: hsl(0 0% 4%); - --foreground: hsl(0 0% 89%); - --card: hsl(0 0% 4%); - --card-foreground: hsl(0 0% 89%); - --popover: hsl(0 0% 14.9%); - --popover-foreground: hsl(0 0% 98%); - --primary: hsl(205, 84%, 53%); - --primary-foreground: hsl(0 0% 9%); - --secondary: hsl(0 0% 14.9%); - --secondary-foreground: hsl(0 0% 98%); - --muted: hsl(0 0% 14.9%); - --muted-foreground: hsl(0 0% 63.9%); - --accent: hsl(0 0% 14.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 100% 60%); - --destructive-foreground: hsl(0 0% 98%); - --constructive: hsl(141, 71%, 48%); - --constructive-foreground: hsl(0, 0%, 100%); - --border: hsl(0 0% 17%); - --input: hsl(0 0% 14.9%); - --ring: hsl(0 0% 83.1%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 3.9%); - --sidebar-foreground: hsl(0 0% 98%); - --sidebar-primary: hsl(0 0% 98%); - --sidebar-primary-foreground: hsl(0 0% 9%); - --sidebar-accent: hsl(0 0% 14.9%); - --sidebar-accent-foreground: hsl(0 0% 98%); - --sidebar-border: hsl(0 0% 14.9%); - --sidebar-ring: hsl(0 0% 83.1%); - --panel-background: hsl(0 0% 11%); - --panel-accent: hsl(0 0% 15%); + --background: hsl(0 0% 4%); + --foreground: hsl(0 0% 89%); + --card: hsl(0 0% 4%); + --card-foreground: hsl(0 0% 89%); + --popover: hsl(0 0% 14.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(205, 84%, 53%); + --primary-foreground: hsl(0 0% 9%); + --secondary: hsl(0 0% 14.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(0 0% 14.9%); + --muted-foreground: hsl(0 0% 63.9%); + --accent: hsl(0 0% 14.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 100% 60%); + --destructive-foreground: hsl(0 0% 98%); + --constructive: hsl(141, 71%, 48%); + --constructive-foreground: hsl(0, 0%, 100%); + --border: hsl(0 0% 17%); + --input: hsl(0 0% 14.9%); + --ring: hsl(0 0% 83.1%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 3.9%); + --sidebar-foreground: hsl(0 0% 98%); + --sidebar-primary: hsl(0 0% 98%); + --sidebar-primary-foreground: hsl(0 0% 9%); + --sidebar-accent: hsl(0 0% 14.9%); + --sidebar-accent-foreground: hsl(0 0% 98%); + --sidebar-border: hsl(0 0% 14.9%); + --sidebar-ring: hsl(0 0% 83.1%); + --panel-background: hsl(0 0% 11%); + --panel-accent: hsl(0 0% 15%); + --sidebar: hsl(240 5.9% 10%); } @layer base { - /* + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. @@ -95,155 +98,164 @@ If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentcolor); - } - /* Other default base styles */ - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - /* Prevent back/forward swipe */ - overscroll-behavior-x: contain; - } + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + /* Other default base styles */ + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + /* Prevent back/forward swipe */ + overscroll-behavior-x: contain; + } } @theme inline { - /* Responsive breakpoints */ - --breakpoint-xs: 30rem; + /* Responsive breakpoints */ + --breakpoint-xs: 30rem; - /* Typography */ - --font-sans: var(--font-inter), sans-serif; + /* Typography */ + --font-sans: var(--font-inter), sans-serif; - /* Font sizes */ - --text-base: 0.95rem; - --text-base--line-height: calc(1.5 / 0.95); - --text-xs: 0.8rem; - --text-xs--line-height: calc(1 / 0.8); + /* Font sizes */ + --text-base: 0.95rem; + --text-base--line-height: calc(1.5 / 0.95); + --text-xs: 0.8rem; + --text-xs--line-height: calc(1 / 0.8); - /* Border radius */ - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 8px); + /* Border radius */ + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 8px); - /* Palette mapped to root design tokens */ - --color-background: var(--background); - --color-foreground: var(--foreground); + /* Palette mapped to root design tokens */ + --color-background: var(--background); + --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); - --color-constructive: var(--constructive); - --color-constructive-foreground: var(--constructive-foreground); + --color-constructive: var(--constructive); + --color-constructive-foreground: var(--constructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); - /* Chart colors */ - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); + /* Chart colors */ + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); - /* Sidebar */ - --color-sidebar: var(--sidebar-background); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + /* Sidebar */ + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); - /* Panel */ - --color-panel: var(--panel-background); - --color-panel-accent: var(--panel-accent); + /* Panel */ + --color-panel: var(--panel-background); + --color-panel-accent: var(--panel-accent); - /* Animations */ - --animate-accordion-down: accordion-down 0.2s ease-out; - --animate-accordion-up: accordion-up 0.2s ease-out; + /* Animations */ + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; - @keyframes accordion-down { - from { - height: 0; - } - to { - height: var(--radix-accordion-content-height); - } - } + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } - @keyframes accordion-up { - from { - height: var(--radix-accordion-content-height); - } - to { - height: 0; - } - } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } } @utility scrollbar-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } } @utility scrollbar-x-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar:horizontal { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:horizontal { + display: none; + } } @utility scrollbar-y-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar:vertical { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:vertical { + display: none; + } } @utility scrollbar-thin { - &::-webkit-scrollbar { - width: 6px; - height: 8px; - } - &::-webkit-scrollbar-track { - background: transparent; - } - &::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 4px; - } - &::-webkit-scrollbar-thumb:hover { - background: var(--muted-foreground); - } + &::-webkit-scrollbar { + width: 6px; + height: 8px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; + } + &::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 57e81266..2b3d0c30 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -13,46 +13,46 @@ const siteFont = Inter({ subsets: ["latin"] }); export const metadata = baseMetaData; const protectedRoutes = [ - { - path: "/none", - method: "GET", - }, + { + path: "/none", + method: "GET", + }, ]; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - - - - - - + + + + + + +
+ + +
+ +
+
+

All projects

+
+ +
+
+ + +
+ +
+ + +
+ + +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ Project +
+ 02:14 +
+
+ + +
+
+

Summer Vlog 2025

+
+ + + + +

Created Jan 20, 2026

+
+
+
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ Project +
+ 00:45 +
+
+ +
+
+

Product Launch Teaser

+
+ + + + +

Created Jan 20, 2026

+
+
+
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ Project +
+ 03:30 +
+
+ +
+
+

Podcast Ep. 4

+
+ + + + +

Created Jan 18, 2026

+
+
+
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/html/design2.html b/html/design2.html new file mode 100644 index 00000000..ae1ac98f --- /dev/null +++ b/html/design2.html @@ -0,0 +1,301 @@ + + + + + + Projects - Design 2 + + + + + + + + + + + +
+ +
+
+

All Projects

+
+
+ Sort by: + +
+
+ +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+ + +
+ +
+ +
+ + +
+
+ +
+
+ + +
+ Project + +
+
+
+
+ 1080p +
+
+ + +
+
+

Cyberpunk Intro

+ 00:15 +
+
+
+ + 2h ago +
+
+
JD
+
+
+
+
+ + +
+
+ +
+ +
+
+ +
+ 4K +
+
+ +
+
+

Documentary_Final_v2

+ 45:20 +
+
+
+ + 1d ago +
+
+
JD
+
AK
+
+
+
+
+ + +
+
+ +
+ +
+
+ +
+
+ +
+ Project +
+
+ +
+
+

Tech Review 2025

+ 12:00 +
+
+
+ + 5h ago +
+
+
JD
+
+
+
+
+ +
+
+ + +
+
+ 3 Projects + 1 Selected +
+
+
Online
+ v2.4.0 +
+
+
+ + + + \ No newline at end of file diff --git a/html/design3.html b/html/design3.html new file mode 100644 index 00000000..bbcf6450 --- /dev/null +++ b/html/design3.html @@ -0,0 +1,217 @@ + + + + + + Projects - Design 3 + + + + + + + + + + +
+ + +
+
+

WORK

+

Manage your creative output. Organize, edit, and publish your stories.

+
+
+ +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+ +
+
+ +
+
Project Name
+
Last Edited
+
Duration
+
Actions
+
+ + +
+
+ +
+
+
+ Thumb +
+
+

Nike Commercial_V3

+ Client: Nike +
+
+
Today, 10:23 AM
+
00:30:00
+
+ + +
+
+ + +
+
+ +
+
+
+ Thumb +
+
+

Fashion Week Recap

+ Personal +
+
+
Yesterday
+
02:15:00
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+
+
+

Untitled Project 04

+ Draft +
+
+
Oct 24, 2025
+
--:--:--
+
+ + +
+
+ +
+ + +
+ 1 Selected +
+
+ + + +
+
+ +
+ + + + \ No newline at end of file diff --git a/html/dropdown_designs.html b/html/dropdown_designs.html new file mode 100644 index 00000000..11212633 --- /dev/null +++ b/html/dropdown_designs.html @@ -0,0 +1,98 @@ + + + + + + Dropdown Options + + + + + + +
+

Dropdown Design Explorations

+

Single interaction for the same brand language. Click the button to toggle.

+
+ +
+ + + +
+

Option 1: Soft & Minimal

+
+ + + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index dca0ce35..8de84865 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,16 @@ "build:web": "turbo run build --filter=@opencut/web", "dev:tools": "turbo run dev --filter=@opencut/tools", "build:tools": "turbo run build --filter=@opencut/tools", - "start:tools": "turbo run start --filter=@opencut/tools" + "start:tools": "turbo run start --filter=@opencut/tools", + "lint:web": "biome lint apps/web/src --max-diagnostics=1000", + "lint:web:fix": "biome lint apps/web/src --write --max-diagnostics=1000", + "format:web": "biome format apps/web/src/services/renderer --write --max-diagnostics=1000" }, "dependencies": { "better-auth": "^1.4.15", "next": "^16.1.3" }, "devDependencies": { - "prettier": "^3.8.0", - "prettier-plugin-tailwindcss": "^0.7.2", "turbo": "^2.7.5", "typescript": "5.8.3" }, diff --git a/packages/ui/package.json b/packages/ui/package.json index d5fd27ba..059b78ce 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,6 +9,7 @@ "./icons": "./src/icons/index.tsx" }, "dependencies": { + "@iconify/react": "^6.0.2", "@types/react": "^19.2.7", "react": "^19.2.0" }, diff --git a/packages/ui/src/icons/base-icon.tsx b/packages/ui/src/icons/base-icon.tsx new file mode 100644 index 00000000..9a63a4f6 --- /dev/null +++ b/packages/ui/src/icons/base-icon.tsx @@ -0,0 +1,31 @@ +import { useId, type ReactNode } from "react"; +import type { IconProps } from "./types"; + +type BaseIconProps = Omit & { + title: string; + children: ReactNode; +}; + +export function BaseIcon({ + title, + size = 24, + viewBox = "0 0 24 24", + children, + ...props +}: BaseIconProps) { + const titleId = useId(); + + return ( + + {title} + {children} + + ); +} diff --git a/packages/ui/src/icons/create-icon.tsx b/packages/ui/src/icons/create-icon.tsx new file mode 100644 index 00000000..262ffee7 --- /dev/null +++ b/packages/ui/src/icons/create-icon.tsx @@ -0,0 +1,47 @@ +import { BaseIcon } from "./base-icon"; +import type { IconDefinition, IconNode, IconProps } from "./types"; +import type { JSX } from "react"; + +type CreateIconParams = { + definition: IconDefinition; +}; + +function renderIconNodes({ nodes }: { nodes: IconNode }) { + const elements: Array = []; + + for (const [index, node] of nodes.entries()) { + const Element = node.element; + const children = node.children + ? renderIconNodes({ nodes: node.children }) + : undefined; + + elements.push( + + {children} + , + ); + } + + return elements; +} + +export function createIcon({ definition }: CreateIconParams) { + function Icon({ title, size, ...props }: IconProps) { + const iconTitle = title ?? definition.title; + + return ( + + {renderIconNodes({ nodes: definition.nodes })} + + ); + } + + Icon.displayName = `${definition.title}Icon`; + + return Icon; +} diff --git a/packages/ui/src/icons/index.tsx b/packages/ui/src/icons/index.tsx index 62109ca4..c95d07c8 100644 --- a/packages/ui/src/icons/index.tsx +++ b/packages/ui/src/icons/index.tsx @@ -1,289 +1,54 @@ -export function GoogleIcon({ className }: { className?: string }) { - return ( - - - - - - - ); -} +import { createIcon } from "./create-icon"; +import { ICON_DEFINITIONS } from "./registry"; -export function GithubIcon({ className }: { className?: string }) { - return ( - - - +export const GoogleIcon = createIcon({ + definition: ICON_DEFINITIONS.google, +}); - - - - ); -} +export const GithubIcon = createIcon({ + definition: ICON_DEFINITIONS.github, +}); -export function VercelIcon({ className }: { className?: string }) { - return ( - - - - ); -} +export const VercelIcon = createIcon({ + definition: ICON_DEFINITIONS.vercel, +}); -export function BackgroundIcon({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - ); -} +export const DataBuddyIcon = createIcon({ + definition: ICON_DEFINITIONS.databuddy, +}); -export function MarbleIcon({ - className = "", - size = 32, -}: { - className?: string; - size?: number; -}) { - return ( - - - - - ); -} +export const MarbleIcon = createIcon({ + definition: ICON_DEFINITIONS.marble, +}); -export function DataBuddyIcon({ - className = "", - size = 32, -}: { - className?: string; - size?: number; -}) { - return ( - - - - - ); -} +export const BackgroundIcon = createIcon({ + definition: ICON_DEFINITIONS.background, +}); -export function SocialsIcon({ - className = "", - size = 32, -}: { - className?: string; - size?: number; -}) { - return ( - - - - - - - - - - - - - ); -} +export const SocialsIcon = createIcon({ + definition: ICON_DEFINITIONS.socials, +}); -export function TransitionUpIcon({ - className = "", - size = 16, -}: { - className?: string; - size?: number; -}) { - return ( - - - - - - ); -} +export const TransitionUpIcon = createIcon({ + definition: ICON_DEFINITIONS.transitionUp, +}); -export function MenuIcon({ - className = "", - size = 16, -}: { - className?: string; - size?: number; -}) { - return ( - - - - - ); -} +export const MenuIcon = createIcon({ + definition: ICON_DEFINITIONS.menu, +}); + +export const PencilIcon = createIcon({ + definition: ICON_DEFINITIONS.pencil, +}); + +export const LeftArrowIcon = createIcon({ + definition: ICON_DEFINITIONS.leftArrow, +}); + +export const TrashIcon = createIcon({ + definition: ICON_DEFINITIONS.trash, +}); + +export type { IconDefinition, IconNode, IconProps } from "./types"; +export { BaseIcon } from "./base-icon"; +export { createIcon } from "./create-icon"; diff --git a/packages/ui/src/icons/registry/brand.ts b/packages/ui/src/icons/registry/brand.ts new file mode 100644 index 00000000..be6f1984 --- /dev/null +++ b/packages/ui/src/icons/registry/brand.ts @@ -0,0 +1,116 @@ +import type { IconDefinition } from "../types"; + +export const BRAND_ICONS = { + google: { + title: "Google", + viewBox: "0 0 24 24", + nodes: [ + { + element: "path", + props: { + fill: "#4285F4", + d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", + }, + }, + { + element: "path", + props: { + fill: "#34A853", + d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", + }, + }, + { + element: "path", + props: { + fill: "#FBBC05", + d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", + }, + }, + { + element: "path", + props: { + fill: "#EA4335", + d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", + }, + }, + ], + }, + github: { + title: "GitHub", + viewBox: "0 -3.5 256 256", + nodes: [ + { + element: "g", + props: { + fill: "currentColor", + }, + children: [ + { + element: "path", + props: { + d: "M127.505 0C57.095 0 0 57.085 0 127.505c0 56.336 36.534 104.13 87.196 120.99 6.372 1.18 8.712-2.766 8.712-6.134 0-3.04-.119-13.085-.173-23.739-35.473 7.713-42.958-15.044-42.958-15.044-5.8-14.738-14.157-18.656-14.157-18.656-11.568-7.914.872-7.752.872-7.752 12.804.9 19.546 13.14 19.546 13.14 11.372 19.493 29.828 13.857 37.104 10.6 1.144-8.242 4.449-13.866 8.095-17.05-28.32-3.225-58.092-14.158-58.092-63.014 0-13.92 4.981-25.295 13.138-34.224-1.324-3.212-5.688-16.18 1.235-33.743 0 0 10.707-3.427 35.073 13.07 10.17-2.826 21.078-4.242 31.914-4.29 10.836.048 21.752 1.464 31.942 4.29 24.337-16.497 35.029-13.07 35.029-13.07 6.94 17.563 2.574 30.531 1.25 33.743 8.175 8.929 13.122 20.303 13.122 34.224 0 48.972-29.828 59.756-58.22 62.912 4.573 3.957 8.648 11.717 8.648 23.612 0 17.06-.148 30.791-.148 34.991 0 3.393 2.295 7.369 8.759 6.117 50.634-16.879 87.122-64.656 87.122-120.973C255.009 57.085 197.922 0 127.505 0", + }, + }, + { + element: "path", + props: { + d: "M47.755 181.634c-.28.633-1.278.823-2.185.389-.925-.416-1.445-1.28-1.145-1.916.275-.652 1.273-.834 2.196-.396.927.415 1.455 1.287 1.134 1.923M54.027 187.23c-.608.564-1.797.302-2.604-.589-.834-.889-.99-2.077-.373-2.65.627-.563 1.78-.3 2.616.59.834.899.996 2.08.36 2.65M58.33 194.39c-.782.543-2.06.034-2.849-1.1-.781-1.133-.781-2.493.017-3.038.792-.545 2.05-.055 2.85 1.07.78 1.153.78 2.513-.019 3.069M65.606 202.683c-.699.77-2.187.564-3.277-.488-1.114-1.028-1.425-2.487-.724-3.258.707-.772 2.204-.555 3.302.488 1.107 1.026 1.445 2.496.7 3.258M75.01 205.483c-.307.998-1.741 1.452-3.185 1.028-1.442-.437-2.386-1.607-2.095-2.616.3-1.005 1.74-1.478 3.195-1.024 1.44.435 2.386 1.596 2.086 2.612M85.714 206.67c.036 1.052-1.189 1.924-2.705 1.943-1.525.033-2.758-.818-2.774-1.852 0-1.062 1.197-1.926 2.721-1.951 1.516-.03 2.758.815 2.758 1.86M96.228 206.267c.182 1.026-.872 2.08-2.377 2.36-1.48.27-2.85-.363-3.039-1.38-.184-1.052.89-2.105 2.367-2.378 1.508-.262 2.857.355 3.049 1.398", + }, + }, + ], + }, + ], + }, + vercel: { + title: "Vercel", + viewBox: "0 0 76 65", + nodes: [ + { + element: "path", + props: { + d: "M37.5274 0L75.0548 65H0L37.5274 0Z", + fill: "currentColor", + }, + }, + ], + }, + marble: { + title: "Marble", + viewBox: "0 0 256 256", + nodes: [ + { + element: "path", + props: { + fill: "#202027", + d: "M0 0h256v256H0z", + }, + }, + { + element: "path", + props: { + fill: "#fff", + d: "M116.032 94.016q1.408.256 6.272 9.856 4.856 9.472 11.904 24.576l4.096 8.576a42.4 42.4 0 0 0 4.992-6.784q2.304-3.96 5.76-10.624 3.96-7.56 5.504-9.728 6.144-9.344 13.312-19.84.896-1.408 3.2-1.92a4.8 4.8 0 0 1 1.28-.128q3.072 0 6.784 2.56 3.84 2.56 4.992 5.248a4.8 4.8 0 0 1 .256 1.92q0 1.28-.128 1.92l-.64 2.944q-.256 1.664-.512 3.968a3200 3200 0 0 0-5.888 46.848 56 56 0 0 0-.512 8.064l-.256 4.096a32 32 0 0 0-.128 2.944q0 1.672-.384 2.304-.384.64-1.408.64-.768 0-2.304-.384-4.096-.896-6.272-3.2-2.176-2.432-2.432-6.656-.128-2.176-.128-6.784 0-4.736.256-14.464l.128-7.04.256-10.112q-1.152 1.024-5.888 8.064a328 328 0 0 0-9.088 14.464q-4.48 7.56-5.888 11.648-1.28 3.2-2.56 4.736-1.152 1.536-2.816 1.536-1.536 0-3.968-1.408-6.912-4.224-8.448-11.648a336 336 0 0 0-6.016-23.68 40 40 0 0 0-2.56-6.272q-1.792-3.968-2.304-4.992l-1.664-3.328q-18.68 24.832-25.344 45.568-1.28 4.352-2.432 6.272-1.152 1.792-2.688 1.792-2.432 0-6.912-4.352Q72 158.144 72 154.304q0-2.04.768-4.096a108 108 0 0 1 16-30.08l3.968-5.248a432 432 0 0 0 12.16-16.64q3.072-4.48 8.192-4.48.896 0 2.944.256", + }, + }, + ], + }, + databuddy: { + title: "Data Buddy", + viewBox: "0 0 8 8", + nodes: [ + { + element: "path", + props: { + d: "M0 0h8v8H0z", + }, + }, + { + element: "path", + props: { + fill: "#fff", + d: "M1 1h1v6H1zm1 0h4v1H2zm4 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zM2 6h4v1H2zm1-3h1v1H3zm1 1h1v1H4z", + }, + }, + ], + }, +} satisfies Record; diff --git a/packages/ui/src/icons/registry/editor.ts b/packages/ui/src/icons/registry/editor.ts new file mode 100644 index 00000000..cb713abb --- /dev/null +++ b/packages/ui/src/icons/registry/editor.ts @@ -0,0 +1,208 @@ +import type { IconDefinition } from "../types"; + +export const EDITOR_ICONS = { + background: { + title: "Background", + viewBox: "0 0 353 353", + nodes: [ + { + element: "g", + props: { + clipPath: "url(#clip0_1_3)", + }, + children: [ + { + element: "rect", + props: { + x: "-241.816", + y: "233.387", + width: "592.187", + height: "17.765", + transform: "rotate(-37 -241.816 233.387)", + fill: "white", + }, + }, + { + element: "rect", + props: { + x: "-189.907", + y: "306.804", + width: "592.187", + height: "17.765", + transform: "rotate(-37 -189.907 306.804)", + fill: "white", + }, + }, + { + element: "rect", + props: { + x: "-146.928", + y: "389.501", + width: "592.187", + height: "17.765", + transform: "rotate(-37 -146.928 389.501)", + fill: "white", + }, + }, + { + element: "rect", + props: { + x: "-103.144", + y: "477.904", + width: "592.187", + height: "17.765", + transform: "rotate(-37 -103.144 477.904)", + fill: "white", + }, + }, + { + element: "rect", + props: { + x: "-57.169", + y: "570.714", + width: "592.187", + height: "17.765", + transform: "rotate(-37 -57.169 570.714)", + fill: "white", + }, + }, + ], + }, + { + element: "defs", + props: {}, + children: [ + { + element: "clipPath", + props: { + id: "clip0_1_3", + }, + children: [ + { + element: "rect", + props: { + width: "353", + height: "353", + fill: "white", + }, + }, + ], + }, + ], + }, + ], + }, + socials: { + title: "Socials", + viewBox: "0 0 345 243", + nodes: [ + { + element: "g", + props: { + opacity: "0.5", + }, + children: [ + { + element: "path", + props: { + d: "M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z", + fill: "#FFFC00", + }, + }, + { + element: "path", + props: { + d: "M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z", + fill: "white", + }, + }, + { + element: "path", + props: { + d: "M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z", + stroke: "black", + strokeWidth: "7", + }, + }, + { + element: "path", + props: { + d: "M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z", + stroke: "black", + strokeWidth: "7", + }, + }, + ], + }, + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M133.5 4H321.5C334.48 4 345 14.5205 345 27.5V215.5C345 228.48 334.48 239 321.5 239H133.5C120.52 239 110 228.48 110 215.5V27.5C110 14.5205 120.52 4 133.5 4Z", + fill: "#010101", + }, + }, + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M261.497 99.4086C271.784 106.758 284.388 111.083 297.999 111.083V84.9035C295.423 84.9045 292.854 84.6357 290.333 84.1016V104.709C276.723 104.709 264.121 100.384 253.831 93.0345V146.459C253.831 173.185 232.155 194.85 205.417 194.85C195.44 194.85 186.167 191.835 178.464 186.665C187.256 195.65 199.516 201.224 213.08 201.224C239.821 201.224 261.498 179.56 261.498 152.833L261.497 99.4086ZM270.953 72.9965C265.696 67.2559 262.244 59.8365 261.497 51.634V48.267H254.233C256.061 58.6916 262.298 67.5981 270.953 72.9965ZM195.376 166.157C192.438 162.308 190.851 157.598 190.858 152.756C190.858 140.533 200.773 130.622 213.005 130.622C215.285 130.621 217.551 130.97 219.723 131.659V104.894C217.185 104.547 214.622 104.399 212.061 104.454V125.286C209.887 124.597 207.62 124.247 205.34 124.249C193.107 124.249 183.193 134.159 183.193 146.384C183.193 155.027 188.149 162.512 195.376 166.157Z", + fill: "#EE1D52", + }, + }, + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M253.831 93.0345C264.121 100.384 276.723 104.709 290.333 104.709V84.1016C282.736 82.4848 276.011 78.5162 270.953 72.9965C262.298 67.5981 256.061 58.6916 254.233 48.267H235.152V152.832C235.108 165.022 225.21 174.892 213.004 174.892C205.811 174.892 199.422 171.465 195.376 166.157C188.149 162.512 183.193 155.027 183.193 146.384C183.193 134.159 193.107 124.249 205.34 124.249C207.683 124.249 209.942 124.614 212.061 125.286V104.454C185.792 104.996 164.665 126.449 164.665 152.833C164.665 166.003 169.926 177.942 178.464 186.665C186.167 191.835 195.44 194.85 205.417 194.85C232.155 194.85 253.831 173.185 253.831 146.459V93.0345Z", + fill: "white", + }, + }, + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M290.333 84.1016V78.5293C283.482 78.5396 276.766 76.6229 270.953 72.9965C276.099 78.6269 282.874 82.5087 290.333 84.1016ZM254.233 48.267C254.058 47.2708 253.924 46.2679 253.831 45.2608V41.8938H227.485V146.459C227.443 158.648 217.545 168.518 205.339 168.518C201.754 168.518 198.372 167.669 195.376 166.157C199.422 171.465 205.811 174.892 213.004 174.892C225.21 174.892 235.108 165.022 235.152 152.832V48.267H254.233ZM212.061 104.454L212.061 98.5212C209.859 98.2202 207.64 98.0698 205.418 98.071C178.676 98.071 157 119.736 157 146.459C157 163.214 165.518 177.979 178.464 186.665C169.926 177.942 164.666 166.002 164.666 152.832C164.666 126.449 185.791 104.996 212.061 104.454Z", + fill: "#69C9D0", + }, + }, + ], + }, + transitionUp: { + title: "Transition Up", + viewBox: "0 0 16 16", + nodes: [ + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M2.5 12.6667C2.5 13.4951 3.17157 14.1667 4 14.1667H12C12.8284 14.1667 13.5 13.4951 13.5 12.6667V11.3333C13.5 10.5049 12.8284 9.83333 12 9.83333H4C3.17157 9.83333 2.5 10.5049 2.5 11.3333V12.6667ZM4 15.1667C2.61929 15.1667 1.5 14.0474 1.5 12.6667V11.3333C1.5 9.95262 2.61929 8.83333 4 8.83333H12C13.3807 8.83333 14.5 9.95262 14.5 11.3333V12.6667C14.5 14.0474 13.3807 15.1667 12 15.1667H4Z", + fill: "white", + }, + }, + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M2 5.83333C1.72386 5.83333 1.5 5.60947 1.5 5.33333L1.5 4C1.5 2.2511 2.91777 0.833332 4.66667 0.833332L11.3333 0.833332C13.0822 0.833332 14.5 2.2511 14.5 4V5.33333C14.5 5.60947 14.2761 5.83333 14 5.83333C13.7239 5.83333 13.5 5.60947 13.5 5.33333V4C13.5 2.80338 12.53 1.83333 11.3333 1.83333L4.66667 1.83333C3.47005 1.83333 2.5 2.80338 2.5 4V5.33333C2.5 5.60947 2.27614 5.83333 2 5.83333Z", + fill: "white", + }, + }, + { + element: "path", + props: { + fillRule: "evenodd", + clipRule: "evenodd", + d: "M8.35355 3.64645C8.15829 3.45118 7.84171 3.45118 7.64645 3.64645L5.64645 5.64645C5.45118 5.84171 5.45118 6.15829 5.64645 6.35355C5.84171 6.54882 6.15829 6.54882 6.35355 6.35355L7.5 5.20711L7.5 9.33333C7.5 9.60948 7.72386 9.83333 8 9.83333C8.27614 9.83333 8.5 9.60948 8.5 9.33333V5.20711L9.64645 6.35355C9.84171 6.54882 10.1583 6.54882 10.3536 6.35355C10.5488 6.15829 10.5488 5.84171 10.3536 5.64645L8.35355 3.64645Z", + fill: "white", + }, + }, + ], + }, +} satisfies Record; diff --git a/packages/ui/src/icons/registry/index.ts b/packages/ui/src/icons/registry/index.ts new file mode 100644 index 00000000..51a38913 --- /dev/null +++ b/packages/ui/src/icons/registry/index.ts @@ -0,0 +1,8 @@ +import type { IconDefinition } from "../types"; +import { BRAND_ICONS } from "./brand"; +import { UI_ICONS } from "./ui"; + +export const ICON_DEFINITIONS = { + ...BRAND_ICONS, + ...UI_ICONS, +} satisfies Record; diff --git a/packages/ui/src/icons/registry/ui.ts b/packages/ui/src/icons/registry/ui.ts new file mode 100644 index 00000000..21e29190 --- /dev/null +++ b/packages/ui/src/icons/registry/ui.ts @@ -0,0 +1,96 @@ +import type { IconDefinition } from "../types"; + +export const UI_ICONS = { + menu: { + title: "Menu", + viewBox: "0 0 141 111", + nodes: [ + { + element: "rect", + props: { + y: "19.5", + width: "141", + height: "12", + rx: "7.5", + fill: "currentColor", + }, + }, + { + element: "rect", + props: { + y: "67.5", + width: "141", + height: "12", + rx: "7.5", + fill: "currentColor", + }, + }, + ], + }, + pencil: { + title: "Edit", + viewBox: "0 0 24 24", + nodes: [ + { + element: "path", + props: { + d: "M14.0737 3.88545C14.8189 3.07808 15.1915 2.6744 15.5874 2.43893C16.5427 1.87076 17.7191 1.85309 18.6904 2.39232C19.0929 2.6158 19.4769 3.00812 20.245 3.79276C21.0131 4.5774 21.3972 4.96972 21.6159 5.38093C22.1438 6.37312 22.1265 7.57479 21.5703 8.5507C21.3398 8.95516 20.9446 9.33578 20.1543 10.097L10.7506 19.1543C9.25288 20.5969 8.504 21.3182 7.56806 21.6837C6.63212 22.0493 5.6032 22.0224 3.54536 21.9686L3.26538 21.9613C2.63891 21.9449 2.32567 21.9367 2.14359 21.73C1.9615 21.5234 1.98636 21.2043 2.03608 20.5662L2.06308 20.2197C2.20301 18.4235 2.27297 17.5255 2.62371 16.7182C2.97444 15.9109 3.57944 15.2555 4.78943 13.9445L14.0737 3.88545Z", + }, + }, + { + element: "path", + props: { + d: "M13 4L20 11", + }, + }, + ], + }, + leftArrow: { + title: "Left Arrow", + viewBox: "0 0 24 24", + nodes: [ + { + element: "path", + props: { + d: "M5.5 12.002H19", + }, + }, + { + element: "path", + props: { + d: "M10.9999 18.002C10.9999 18.002 4.99998 13.583 4.99997 12.0019C4.99996 10.4208 11 6.00195 11 6.00195", + }, + }, + ], + }, + trash: { + title: "Trash", + viewBox: "0 0 24 24", + nodes: [ + { + element: "path", + props: { + d: "M19.5 5.5L18.8803 15.5251C18.7219 18.0864 18.6428 19.3671 18.0008 20.2879C17.6833 20.7431 17.2747 21.1273 16.8007 21.416C15.8421 22 14.559 22 11.9927 22C9.42312 22 8.1383 22 7.17905 21.4149C6.7048 21.1257 6.296 20.7408 5.97868 20.2848C5.33688 19.3626 5.25945 18.0801 5.10461 15.5152L4.5 5.5", + }, + }, + { + element: "path", + props: { + d: "M3 5.5H21M16.0557 5.5L15.3731 4.09173C14.9196 3.15626 14.6928 2.68852 14.3017 2.39681C14.215 2.3321 14.1231 2.27454 14.027 2.2247C13.5939 2 13.0741 2 12.0345 2C10.9688 2 10.436 2 9.99568 2.23412C9.8981 2.28601 9.80498 2.3459 9.71729 2.41317C9.32164 2.7167 9.10063 3.20155 8.65861 4.17126L8.05292 5.5", + }, + }, + { + element: "path", + props: { + d: "M9.5 16.5L9.5 10.5", + }, + }, + { + element: "path", + props: { + d: "M14.5 16.5L14.5 10.5", + }, + }, + ], + }, +} satisfies Record; diff --git a/packages/ui/src/icons/types.ts b/packages/ui/src/icons/types.ts new file mode 100644 index 00000000..a0fc4e42 --- /dev/null +++ b/packages/ui/src/icons/types.ts @@ -0,0 +1,21 @@ +import type { SVGProps, JSX } from "react"; + +export type IconProps = Omit< + SVGProps, + "children" | "width" | "height" +> & { + size?: number; + title?: string; +}; + +export type IconNode = Array<{ + element: keyof JSX.IntrinsicElements; + props: Record; + children?: IconNode; +}>; + +export type IconDefinition = { + title: string; + viewBox?: string; + nodes: IconNode; +}; diff --git a/rules.md b/rules.md index 3f95b911..f934c1cf 100644 --- a/rules.md +++ b/rules.md @@ -1,6 +1,6 @@ Review every point below carefully for files to ensure they follow consistent code style and best practices. -1. Every single function must use a destructured props object for parameters, regardless of whether there's one or multiple parameters. This applies to all functions, not just react components. Even zustand stores. exception: tiny one-liner callbacks (e.g. `array.find(x => ...)`, `map`, `filter`, `sort`) do not need destructuring if it hurts readability or adds noise. +1. Every single function must accept a single object parameter and destructure it in the signature (for readability and future extensibility), if there's 1 or more values. This applies to all functions, not just react components. Even zustand stores. exception: tiny one-liner callbacks (e.g. `array.find(x => ...)`, `map`, `filter`, `sort`) do not need destructuring if it hurts readability or adds noise. 2. No `any` references. 3. JSX is clean; No comments in there explaining what each part does. Must extract JSX into sub-components (always placed below the main component). A part only needs to be extracted if it's either used in multiple places of the file OR is complex enough to be worth extracting. If a component is used acrosss multiple files, it should be extracted to a separate file. 4. General interfaces must be placed in the `types` folder. Example: `src/components/timeline/index.tsx` defining a `TimelineTrack` interface is wrong and should be moved to `src/types/timeline.ts`.