Skip to content

Commit

Permalink
feat: added resizable & progress-circle components
Browse files Browse the repository at this point in the history
  • Loading branch information
sek-consulting committed Apr 29, 2024
1 parent c2e694b commit ca5a390
Show file tree
Hide file tree
Showing 22 changed files with 627 additions and 65 deletions.
3 changes: 2 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"chart.js": "^4.4.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"corvu": "^0.4.8",
"cmdk-solid": "^1.0.1",
"corvu": "^0.5.0",
"embla-carousel-autoplay": "^8.0.0",
"embla-carousel-solid": "^8.0.0",
"shosho": "^1.4.3",
Expand Down
30 changes: 30 additions & 0 deletions apps/docs/public/registry/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@
],
"type": "ui"
},
{
"name": "progress-circle",
"dependencies": [
"@kobalte/core"
],
"files": [
"ui/progress-circle.tsx"
],
"type": "ui"
},
{
"name": "radio-group",
"dependencies": [
Expand All @@ -293,6 +303,16 @@
],
"type": "ui"
},
{
"name": "resizable",
"dependencies": [
"corvu"
],
"files": [
"ui/resizable.tsx"
],
"type": "ui"
},
{
"name": "select",
"dependencies": [
Expand Down Expand Up @@ -401,6 +421,16 @@
],
"type": "ui"
},
{
"name": "toggle-group",
"dependencies": [
"@kobalte/core"
],
"files": [
"ui/toggle-group.tsx"
],
"type": "ui"
},
{
"name": "tooltip",
"dependencies": [
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/public/registry/ui/carousel.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"files": [
{
"name": "carousel.tsx",
"content": "import type { Accessor, Component, ComponentProps, VoidProps } from \"solid-js\"\nimport {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n mergeProps,\n splitProps,\n useContext\n} from \"solid-js\"\n\nimport type { CreateEmblaCarouselType } from \"embla-carousel-solid\"\nimport createEmblaCarousel from \"embla-carousel-solid\"\n\nimport { cn } from \"~/lib/utils\"\nimport { Button, ButtonProps } from \"~/registry/ui/button\"\n\nexport type CarouselApi = CreateEmblaCarouselType[1]\n\ntype UseCarouselParameters = Parameters<typeof createEmblaCarousel>\ntype CarouselOptions = NonNullable<UseCarouselParameters[0]>\ntype CarouselPlugin = NonNullable<UseCarouselParameters[1]>\n\ntype CarouselProps = {\n opts?: ReturnType<CarouselOptions>\n plugins?: ReturnType<CarouselPlugin>\n orientation?: \"horizontal\" | \"vertical\"\n setApi?: (api: CarouselApi) => void\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof createEmblaCarousel>[0]\n api: ReturnType<typeof createEmblaCarousel>[1]\n scrollPrev: () => void\n scrollNext: () => void\n canScrollPrev: Accessor<boolean>\n canScrollNext: Accessor<boolean>\n} & CarouselProps\n\nconst CarouselContext = createContext<Accessor<CarouselContextProps> | null>(null)\n\nconst useCarousel = () => {\n const context = useContext(CarouselContext)\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\")\n }\n\n return context()\n}\n\nconst Carousel: Component<CarouselProps & ComponentProps<\"div\">> = (rawProps) => {\n const props = mergeProps<(CarouselProps & ComponentProps<\"div\">)[]>(\n { orientation: \"horizontal\" },\n rawProps\n )\n\n const [, rest] = splitProps(props, [\n \"orientation\",\n \"opts\",\n \"setApi\",\n \"plugins\",\n \"class\",\n \"children\"\n ])\n\n const [carouselRef, api] = createEmblaCarousel(\n () => ({\n ...props.opts,\n axis: props.orientation === \"horizontal\" ? \"x\" : \"y\"\n }),\n () => (props.plugins === undefined ? [] : props.plugins)\n )\n const [canScrollPrev, setCanScrollPrev] = createSignal(false)\n const [canScrollNext, setCanScrollNext] = createSignal(false)\n\n const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {\n setCanScrollPrev(api.canScrollPrev())\n setCanScrollNext(api.canScrollNext())\n }\n\n const scrollPrev = () => {\n api()?.scrollPrev()\n }\n\n const scrollNext = () => {\n api()?.scrollNext()\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault()\n scrollPrev()\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault()\n scrollNext()\n }\n }\n\n createEffect(() => {\n if (!api() || !props.setApi) {\n return\n }\n\n props.setApi(api)\n })\n\n createEffect(() => {\n if (!api()) {\n return\n }\n\n onSelect(api()!)\n api()!.on(\"reInit\", onSelect)\n api()!.on(\"select\", onSelect)\n\n return () => {\n api()?.off(\"select\", onSelect)\n }\n })\n\n const value = createMemo(\n () =>\n ({\n carouselRef,\n api,\n opts: props.opts,\n orientation: props.orientation || (props.opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext\n }) satisfies CarouselContextProps\n )\n\n return (\n <CarouselContext.Provider value={value}>\n <div\n onKeyDown={handleKeyDown}\n class={cn(\"relative\", props.class)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n {...rest}\n >\n {props.children}\n </div>\n </CarouselContext.Provider>\n )\n}\n\nconst CarouselContent: Component<ComponentProps<\"div\">> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n const { carouselRef, orientation } = useCarousel()\n\n return (\n <div ref={carouselRef} class=\"overflow-hidden\">\n <div\n class={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", props.class)}\n {...rest}\n />\n </div>\n )\n}\n\nconst CarouselItem: Component<ComponentProps<\"div\">> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n const { orientation } = useCarousel()\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n class={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n props.class\n )}\n {...rest}\n />\n )\n}\n\ntype CarouselButtonProps = VoidProps<ButtonProps>\n\nconst CarouselPrevious: Component<CarouselButtonProps> = (rawProps) => {\n const props = mergeProps<CarouselButtonProps[]>({ variant: \"outline\", size: \"icon\" }, rawProps)\n const [, rest] = splitProps(props, [\"class\", \"variant\", \"size\"])\n const { orientation, scrollPrev, canScrollPrev } = useCarousel()\n\n return (\n <Button\n variant={props.variant}\n size={props.size}\n class={cn(\n \"absolute size-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"-left-12 top-1/2 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n props.class\n )}\n disabled={!canScrollPrev()}\n onClick={scrollPrev}\n {...rest}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"size-4\"\n >\n <path d=\"M5 12l14 0\" />\n <path d=\"M5 12l6 6\" />\n <path d=\"M5 12l6 -6\" />\n </svg>\n <span class=\"sr-only\">Previous slide</span>\n </Button>\n )\n}\n\nconst CarouselNext: Component<CarouselButtonProps> = (rawProps) => {\n const props = mergeProps<CarouselButtonProps[]>({ variant: \"outline\", size: \"icon\" }, rawProps)\n const [, rest] = splitProps(props, [\"class\", \"variant\", \"size\"])\n const { orientation, scrollNext, canScrollNext } = useCarousel()\n\n return (\n <Button\n variant={props.variant}\n size={props.size}\n class={cn(\n \"absolute size-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"-right-12 top-1/2 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n props.class\n )}\n disabled={!canScrollNext()}\n onClick={scrollNext}\n {...rest}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"size-4\"\n >\n <path d=\"M5 12l14 0\" />\n <path d=\"M13 18l6 -6\" />\n <path d=\"M13 6l6 6\" />\n </svg>\n <span class=\"sr-only\">Next slide</span>\n </Button>\n )\n}\n\nexport { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }\n"
"content": "import type { Accessor, Component, ComponentProps, VoidProps } from \"solid-js\"\nimport {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n mergeProps,\n splitProps,\n useContext\n} from \"solid-js\"\n\nimport type { CreateEmblaCarouselType } from \"embla-carousel-solid\"\nimport createEmblaCarousel from \"embla-carousel-solid\"\n\nimport { cn } from \"~/lib/utils\"\nimport { Button, ButtonProps } from \"~/registry/ui/button\"\n\nexport type CarouselApi = CreateEmblaCarouselType[1]\n\ntype UseCarouselParameters = Parameters<typeof createEmblaCarousel>\ntype CarouselOptions = NonNullable<UseCarouselParameters[0]>\ntype CarouselPlugin = NonNullable<UseCarouselParameters[1]>\n\ntype CarouselProps = {\n opts?: ReturnType<CarouselOptions>\n plugins?: ReturnType<CarouselPlugin>\n orientation?: \"horizontal\" | \"vertical\"\n setApi?: (api: CarouselApi) => void\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof createEmblaCarousel>[0]\n api: ReturnType<typeof createEmblaCarousel>[1]\n scrollPrev: () => void\n scrollNext: () => void\n canScrollPrev: Accessor<boolean>\n canScrollNext: Accessor<boolean>\n} & CarouselProps\n\nconst CarouselContext = createContext<Accessor<CarouselContextProps> | null>(null)\n\nconst useCarousel = () => {\n const context = useContext(CarouselContext)\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\")\n }\n\n return context()\n}\n\nconst Carousel: Component<CarouselProps & ComponentProps<\"div\">> = (rawProps) => {\n const props = mergeProps<(CarouselProps & ComponentProps<\"div\">)[]>(\n { orientation: \"horizontal\" },\n rawProps\n )\n\n const [, rest] = splitProps(props, [\n \"orientation\",\n \"opts\",\n \"setApi\",\n \"plugins\",\n \"class\",\n \"children\"\n ])\n\n const [carouselRef, api] = createEmblaCarousel(\n () => ({\n ...props.opts,\n axis: props.orientation === \"horizontal\" ? \"x\" : \"y\"\n }),\n () => (props.plugins === undefined ? [] : props.plugins)\n )\n const [canScrollPrev, setCanScrollPrev] = createSignal(false)\n const [canScrollNext, setCanScrollNext] = createSignal(false)\n\n const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {\n setCanScrollPrev(api.canScrollPrev())\n setCanScrollNext(api.canScrollNext())\n }\n\n const scrollPrev = () => {\n api()?.scrollPrev()\n }\n\n const scrollNext = () => {\n api()?.scrollNext()\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault()\n scrollPrev()\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault()\n scrollNext()\n }\n }\n\n createEffect(() => {\n if (!api() || !props.setApi) {\n return\n }\n\n props.setApi(api)\n })\n\n createEffect(() => {\n if (!api()) {\n return\n }\n\n onSelect(api()!)\n api()!.on(\"reInit\", onSelect)\n api()!.on(\"select\", onSelect)\n\n return () => {\n api()?.off(\"select\", onSelect)\n }\n })\n\n const value = createMemo(\n () =>\n ({\n carouselRef,\n api,\n opts: props.opts,\n orientation: props.orientation || (props.opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext\n }) satisfies CarouselContextProps\n )\n\n return (\n <CarouselContext.Provider value={value}>\n <div\n onKeyDown={handleKeyDown}\n class={cn(\"relative\", props.class)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n {...rest}\n >\n {props.children}\n </div>\n </CarouselContext.Provider>\n )\n}\n\nconst CarouselContent: Component<ComponentProps<\"div\">> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n const { carouselRef, orientation } = useCarousel()\n\n return (\n <div ref={carouselRef} class=\"overflow-hidden\">\n <div\n class={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", props.class)}\n {...rest}\n />\n </div>\n )\n}\n\nconst CarouselItem: Component<ComponentProps<\"div\">> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n const { orientation } = useCarousel()\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n class={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n props.class\n )}\n {...rest}\n />\n )\n}\n\ntype CarouselButtonProps = VoidProps<ButtonProps>\n\nconst CarouselPrevious: Component<CarouselButtonProps> = (rawProps) => {\n const props = mergeProps<CarouselButtonProps[]>({ variant: \"outline\", size: \"icon\" }, rawProps)\n const [, rest] = splitProps(props, [\"class\", \"variant\", \"size\"])\n const { orientation, scrollPrev, canScrollPrev } = useCarousel()\n\n return (\n <Button\n variant={props.variant}\n size={props.size}\n class={cn(\n \"absolute size-8 rounded-full touch-manipulation\",\n orientation === \"horizontal\"\n ? \"-left-12 top-1/2 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n props.class\n )}\n disabled={!canScrollPrev()}\n onClick={scrollPrev}\n {...rest}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"size-4\"\n >\n <path d=\"M5 12l14 0\" />\n <path d=\"M5 12l6 6\" />\n <path d=\"M5 12l6 -6\" />\n </svg>\n <span class=\"sr-only\">Previous slide</span>\n </Button>\n )\n}\n\nconst CarouselNext: Component<CarouselButtonProps> = (rawProps) => {\n const props = mergeProps<CarouselButtonProps[]>({ variant: \"outline\", size: \"icon\" }, rawProps)\n const [, rest] = splitProps(props, [\"class\", \"variant\", \"size\"])\n const { orientation, scrollNext, canScrollNext } = useCarousel()\n\n return (\n <Button\n variant={props.variant}\n size={props.size}\n class={cn(\n \"absolute size-8 rounded-full touch-manipulation\",\n orientation === \"horizontal\"\n ? \"-right-12 top-1/2 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n props.class\n )}\n disabled={!canScrollNext()}\n onClick={scrollNext}\n {...rest}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"size-4\"\n >\n <path d=\"M5 12l14 0\" />\n <path d=\"M13 18l6 -6\" />\n <path d=\"M13 6l6 6\" />\n </svg>\n <span class=\"sr-only\">Next slide</span>\n </Button>\n )\n}\n\nexport { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }\n"
}
],
"type": "ui"
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/public/registry/ui/progress-circle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "progress-circle",
"dependencies": [
"@kobalte/core"
],
"files": [
{
"name": "progress-circle.tsx",
"content": "import { Component, ComponentProps, mergeProps, splitProps } from \"solid-js\"\n\nimport { cn } from \"~/lib/utils\"\n\ntype Size = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst sizes: Record<Size, { radius: number; strokeWidth: number }> = {\n xs: { radius: 15, strokeWidth: 3 },\n sm: { radius: 19, strokeWidth: 4 },\n md: { radius: 32, strokeWidth: 6 },\n lg: { radius: 52, strokeWidth: 8 },\n xl: { radius: 80, strokeWidth: 10 }\n}\n\nexport interface ProgressCircleProps extends ComponentProps<\"div\"> {\n value?: number\n size?: Size\n radius?: number\n strokeWidth?: number\n showAnimation?: boolean\n}\n\nconst ProgressCircle: Component<ProgressCircleProps> = (rawProps) => {\n const props = mergeProps({ size: \"md\" as Size, showAnimation: true }, rawProps)\n const [, rest] = splitProps(props, [\n \"class\",\n \"children\",\n \"value\",\n \"size\",\n \"radius\",\n \"strokeWidth\",\n \"showAnimation\"\n ])\n\n const value = () => getLimitedValue(props.value)\n const radius = () => props.radius ?? sizes[props.size].radius\n const strokeWidth = () => props.strokeWidth ?? sizes[props.size].strokeWidth\n const normalizedRadius = () => radius() - strokeWidth() / 2\n const circumference = () => normalizedRadius() * 2 * Math.PI\n const strokeDashoffset = () => (value() / 100) * circumference()\n const offset = () => circumference() - strokeDashoffset()\n\n return (\n <div class={cn(\"flex flex-col items-center justify-center\", props.class)} {...rest}>\n <svg\n width={radius() * 2}\n height={radius() * 2}\n viewBox={`0 0 ${radius() * 2} ${radius() * 2}`}\n class=\"-rotate-90\"\n >\n <circle\n r={normalizedRadius()}\n cx={radius()}\n cy={radius()}\n stroke-width={strokeWidth()}\n fill=\"transparent\"\n stroke=\"\"\n stroke-linecap=\"round\"\n class={cn(\"stroke-secondary transition-colors ease-linear\")}\n />\n {value() >= 0 ? (\n <circle\n r={normalizedRadius()}\n cx={radius()}\n cy={radius()}\n stroke-width={strokeWidth()}\n stroke-dasharray={circumference() + \" \" + circumference()}\n stroke-dashoffset={offset()}\n fill=\"transparent\"\n stroke=\"\"\n stroke-linecap=\"round\"\n class={cn(\n \"stroke-primary transition-colors ease-linear\",\n props.showAnimation ? \"transition-all duration-300 ease-in-out\" : \"\"\n )}\n />\n ) : null}\n </svg>\n <div class={cn(\"absolute flex\")}>{props.children}</div>\n </div>\n )\n}\n\nfunction getLimitedValue(input: number | undefined) {\n if (input === undefined) {\n return 0\n } else if (input > 100) {\n return 100\n }\n return input\n}\n\nexport { ProgressCircle }\n"
}
],
"type": "ui"
}
Loading

0 comments on commit ca5a390

Please sign in to comment.