feat(ui): implement new dialog and dropdown components

This commit is contained in:
ZhymabekRoman 2024-10-19 23:29:36 +05:00
parent fc23d49ebe
commit 74f12e2c1d
50 changed files with 1552 additions and 47 deletions

2
.gitignore vendored
View file

@ -14,7 +14,7 @@ dist/
downloads/
eggs/
.eggs/
lib/
# lib/
lib64/
parts/
sdist/

Binary file not shown.

View file

@ -17,7 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"autoprefixer": "^10.4.20",
"clsx": "^2.1.1",
"lucide-svelte": "^0.452.0",
"lucide-svelte": "^0.453.0",
"mode-watcher": "^0.4.1",
"postcss": "^8.4.47",
"prettier": "^3.1.1",
@ -34,6 +34,8 @@
"type": "module",
"dependencies": {
"@iconify/svelte": "^4.0.2",
"bits-ui": "^0.21.16"
"bits-ui": "^0.21.16",
"copy-to-clipboard": "^3.3.3",
"svelte-bricks": "^0.2.1"
}
}

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -0,0 +1,49 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import X from 'lucide-svelte/icons/x';
import * as Dialog from './index.js';
import { cn, flyAndScale } from '$lib/utils.js';
type $$Props = DialogPrimitive.ContentProps;
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props['transitionConfig'] = {
duration: 200
};
export { className as class };
</script>
<Dialog.Portal>
<Dialog.Overlay />
<DialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
'bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full',
'max-h-[80dvh] overflow-y-auto',
className
)}
{...$$restProps}
>
<div class="relative">
<slot />
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-1 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
</DialogPrimitive.Content>
</Dialog.Portal>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Description>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.OverlayProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class };
</script>
<DialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
type $$Props = DialogPrimitive.PortalProps;
</script>
<DialogPrimitive.Portal {...$$restProps}>
<slot />
</DialogPrimitive.Portal>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DialogPrimitive.Title
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Title>

View file

@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Portal from "./dialog-portal.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</DropdownMenuPrimitive.Content>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</DropdownMenuPrimitive.Label>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</DropdownMenuPrimitive.RadioGroup>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Separator
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
<slot />
</span>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</DropdownMenuPrimitive.SubContent>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,48 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
};

View file

@ -0,0 +1,29 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View file

@ -0,0 +1,15 @@
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import Root from "./radio-group.svelte";
import Item from "./radio-group-item.svelte";
const Input = RadioGroupPrimitive.Input;
export {
Root,
Input,
Item,
//
Root as RadioGroup,
Input as RadioGroupInput,
Item as RadioGroupItem,
};

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = RadioGroupPrimitive.ItemProps;
type $$Events = RadioGroupPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<RadioGroupPrimitive.Item
{value}
class={cn(
"border-primary text-primary ring-offset-background focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
on:click
>
<div class="flex items-center justify-center">
<RadioGroupPrimitive.ItemIndicator>
<Circle class="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.ItemIndicator>
</div>
</RadioGroupPrimitive.Item>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = RadioGroupPrimitive.Props;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
</script>
<RadioGroupPrimitive.Root bind:value class={cn("grid gap-2", className)} {...$$restProps}>
<slot />
</RadioGroupPrimitive.Root>

View file

@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
type $$Props = SonnerProps;
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...$$restProps}
/>

View file

@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View file

@ -0,0 +1,38 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<textarea
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View file

@ -0,0 +1,5 @@
<div class="w-full bg-yellow-400 text-center py-1 px-4">
<p class="text-yellow-900">
Advertise here and support our project! Reach out to us at admin@freedium.cfd
</p>
</div>

View file

@ -0,0 +1,70 @@
<script lang="ts">
import Icon from '@iconify/svelte';
const sizes = ['small', 'medium', 'large'];
export let id: number;
export let title: string;
export let excerpt: string;
export let imageUrl: string;
export let bottomImageUrl: string | null = null;
export let size: 'small' | 'medium' | 'large' | null = randomSize();
export let readingTime: string;
export let publishedAt: string;
export let collection: { name: string; avatarId: string } | null = null;
export let creator: string;
export let slug: string;
const sizeClasses = {
small: 'w-full',
medium: 'w-full',
large: 'w-full'
};
const imageClasses = {
small: 'h-32',
medium: 'h-48',
large: 'h-64'
};
function randomSize(): 'small' | 'medium' | 'large' {
return sizes[Math.floor(Math.random() * sizes.length)] as 'small' | 'medium' | 'large';
}
</script>
<a href={`/${slug}`} class="block no-underline">
<div
class={`border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 rounded-lg shadow-md overflow-hidden ${sizeClasses[size]} transition-transform duration-300 ease-in-out hover:scale-105 mb-4`}
>
{#if imageUrl}
<img src={imageUrl} alt={title} class={`w-full object-cover ${imageClasses[size]}`} />
{/if}
<div class="p-4">
<h2 class="mb-2 text-xl font-bold text-zinc-900 dark:text-zinc-100">{title}</h2>
<p class="text-gray-600 dark:text-gray-300">{excerpt}</p>
<div class="flex flex-wrap items-center mt-2 space-x-2 text-sm text-gray-500 dark:text-white">
<Icon icon="mage:medium" class="w-4 h-4 mr-1" />
{#if collection}
<div class="flex items-center">
<img
src="https://miro.medium.com/v2/resize:fill:48:48/{collection.avatarId}"
alt={collection.name.charAt(0)}
loading="eager"
class="w-4 h-4 mr-1 rounded-full no-lightense"
/>
<p>{collection.name}</p>
</div>
<span>·</span>
{/if}
<span>{creator}</span>
<span>·</span>
<span>~{readingTime} min read</span>
<span class="md:inline">·</span>
<span>{publishedAt}</span>
</div>
</div>
{#if bottomImageUrl}
<img src={bottomImageUrl} alt={title} class={`w-full object-cover ${imageClasses[size]}`} />
{/if}
</div>
</a>

View file

@ -0,0 +1,93 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Dialog from '$lib/components/ui/dialog';
import CodeBlock from './CodeBlock.svelte';
import copy from 'copy-to-clipboard';
import { toast } from 'svelte-sonner';
function handleCopy(text: string) {
copy(text);
toast.success('Copied to clipboard');
}
</script>
<Dialog.Root>
<Dialog.Trigger class="w-full">
<DropdownMenu.Item on:click={($event) => $event.preventDefault()}>
<Icon icon="mdi:bookmark" class="w-4 h-4 mr-2" />
<span>Bookmark</span>
</DropdownMenu.Item>
</Dialog.Trigger>
<Dialog.Content
class="w-full max-w-[94%] sm:max-w-[490px] md:w-full bg-white dark:bg-zinc-900 flex flex-col"
>
<Dialog.Header>
<Dialog.Title class="text-lg font-semibold tracking-tight"
>Add Bookmark for Medium Bypass</Dialog.Title
>
</Dialog.Header>
<div class="flex-1">
<Dialog.Description class="space-y-4 text-foreground-alt">
<p class="text-sm italic">
Credit: blazeknifecatcher on <a
href="https://www.reddit.com/r/paywall/comments/15jsr6z/bypass_mediumcom_paywall/"
class="text-blue-500 hover:underline"
target="_blank"
rel="noopener noreferrer">Reddit</a
>
</p>
<div class="p-4 bg-gray-100 rounded-lg dark:bg-zinc-800">
<div class="flex items-center justify-between mb-2">
<h3 class="mt-2 font-semibold">Option 1: Redirect in Current Tab</h3>
<Button
variant="outline"
size="icon"
class="ml-2"
on:click={() =>
handleCopy(
`javascript:window.location="https://freedium.cfd/"+encodeURIComponent(window.location)"`
)}
>
<Icon icon="mdi:content-copy" class="w-4 h-4" />
</Button>
</div>
<p class="mb-2">Create a new bookmark with the following code as the URL:</p>
<CodeBlock
code={`javascript:window.location="https://freedium.cfd/"+encodeURIComponent(window.location)"`}
/>
</div>
<div class="p-4 bg-gray-100 rounded-lg dark:bg-zinc-800">
<div class="flex items-center justify-between mb-2">
<h3 class="mt-2 font-semibold">Option 2: Open in New Tab</h3>
<Button
variant="outline"
size="icon"
class="ml-2"
on:click={() =>
handleCopy(
`javascript:(function(){window.open("https://freedium.cfd/"+encodeURIComponent(window.location))})();`
)}
>
<Icon icon="mdi:content-copy" class="w-4 h-4" />
</Button>
</div>
<p class="mb-2">For opening in a new tab, use this code instead:</p>
<CodeBlock
code={`javascript:(function(){window.open("https://freedium.cfd/"+encodeURIComponent(window.location))})();`}
/>
</div>
<p>Click the bookmark on any Medium page to bypass the paywall using Freedium.</p>
</Dialog.Description>
</div>
<Dialog.Footer class="flex justify-end p-4">
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Close</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View file

@ -0,0 +1,7 @@
<script lang="ts">
export let code: string;
</script>
<pre class="p-2 overflow-x-auto text-sm bg-gray-200 rounded dark:bg-zinc-700">
<code>{code}</code>
</pre>

View file

@ -0,0 +1,56 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { Button } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import BookmarkButton from './BookmarkButton.svelte';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
variant="ghost"
class="px-3 text-gray-600 py-7 dark:text-white hover:text-primary dark:hover:text-primary"
><Icon icon="mdi:puzzle" class="size-5" /></Button
>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56 text-zinc-600 dark:text-zinc-300 ">
<DropdownMenu.Label>Browser Extensions</DropdownMenu.Label>
<DropdownMenu.Item class="text-xs text-gray-600 cursor-default dark:text-gray-500">
<span
>Note: These solutions are not developed or controlled by the Freedium developers team. Use
at your own risk.</span
>
</DropdownMenu.Item>
<DropdownMenu.Item class="text-xs text-gray-600 cursor-default dark:text-gray-500">
<span> Freedium is not responsible for any damages or losses caused by these solutions.</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item>
<Icon icon="mdi:firefox" class="w-4 h-4 mr-2" />
<span>Firefox</span>
<Icon icon="heroicons-outline:external-link" class="size-3.5" />
</DropdownMenu.Item>
<DropdownMenu.Item>
<Icon icon="mdi:google-chrome" class="w-4 h-4 mr-2" />
<span>Chrome</span>
<Icon icon="heroicons-outline:external-link" class="size-3.5" />
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item>
<a
class="flex flex-row items-center"
href="https://gist.github.com/mathix420/e0604ab0e916622972372711d2829555"
target="_blank"
>
<Icon icon="mdi:script-text" class="w-4 h-4 mr-2" />
<span>Userscript - only Medium</span>
<Icon icon="heroicons-outline:external-link" class="size-3.5" />
</a>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<BookmarkButton />
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,54 @@
<script lang="ts">
import Advertise from './Advertise.svelte';
import ProgressLine from './ProgressLine.svelte';
import ThemeToggle from './ThemeToggle.svelte';
import ReportProblem from './ReportProblem.svelte';
import PayButtons from './PayButtons.svelte';
import ExtensionsButton from './ExtensionsButton.svelte';
import { Menu } from 'lucide-svelte';
let isNavOpen = false;
const toggleNav = () => {
isNavOpen = !isNavOpen;
};
</script>
<nav
id="header"
class="sticky top-0 z-20 w-full bg-white border-b shadow-sm dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800"
>
<Advertise />
<ProgressLine />
<div class="container flex items-center justify-between h-16 px-4 mx-auto">
<a class="text-2xl font-bold transition-opacity text-primary hover:opacity-80" href="/"
>Freedium</a
>
<button
id="nav-toggle"
on:click={toggleNav}
class="p-2 transition-colors rounded-full md:hidden hover:bg-zinc-100 dark:hover:bg-zinc-800"
aria-label="Toggle navigation menu"
>
<Menu class="w-6 h-6" />
</button>
<div class="items-center hidden space-x-2 md:flex">
<ThemeToggle />
<ExtensionsButton />
<div class="w-px h-6 bg-zinc-300 dark:bg-zinc-700"></div>
<PayButtons name="Ko-fi" url="https://ko-fi.com/zhymabekroman" icon="teenyicons:cup-solid" />
<PayButtons
name="Liberapay"
url="https://liberapay.com/ZhymabekRoman/"
icon="simple-icons:liberapay"
/>
<PayButtons name="PayPal" url="" icon="simple-icons:paypal" />
<div class="w-px h-6 bg-zinc-300 dark:bg-zinc-700"></div>
<ReportProblem />
</div>
</div>
</nav>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import UrlBox from './UrlBox.svelte';
</script>
<div class="container w-full pt-40 mx-auto md:max-w-3xl"></div>
<div class="container w-full py-20 pt-20 mx-auto break-words">
<div class="flex flex-col items-center justify-center h-60">
<h1 class="mt-8 text-4xl font-bold text-center text-primary md:max-w-3xl">
Freedium: Your paywall breakthrough for Medium!
</h1>
<UrlBox />
</div>
</div>
<!-- <div class="mt-8"></div> -->

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { onMount, afterUpdate } from 'svelte';
export let columnWidth = 300; // Default column width
export let gap = 16; // Gap between items
let container: HTMLElement;
let items: HTMLElement[];
let columns: number = 1;
function updateLayout() {
if (!container) return;
const containerWidth = container.offsetWidth;
columns = Math.floor(containerWidth / (columnWidth + gap));
const actualColumnWidth = (containerWidth - (columns - 1) * gap) / columns;
container.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
container.style.gridGap = `${gap}px`;
items.forEach((item, i) => {
const column = i % columns;
const prevItem = items[i - columns];
const top = prevItem ? prevItem.getBoundingClientRect().bottom + gap : 0;
item.style.gridColumn = `${column + 1}`;
item.style.gridRow = `${Math.floor(top / 10)}`;
});
}
onMount(() => {
items = Array.from(container.children) as HTMLElement[];
updateLayout();
window.addEventListener('resize', updateLayout);
});
afterUpdate(updateLayout);
</script>
<div bind:this={container} class="masonry-grid">
<slot></slot>
</div>
<style>
.masonry-grid {
display: grid;
width: 100%;
}
</style>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import Icon from '@iconify/svelte';
export let name: string;
export let url: string;
export let icon: string;
</script>
<Button variant="ghost" class="w-24 px-3 py-7">
<a
class="transition-colors text-zinc-600 dark:text-zinc-300 hover:text-primary dark:hover:text-primary"
href={url}
target="_blank"
rel="noopener noreferrer"
>
<Icon {icon} class="size-5" />
{name}
</a>
</Button>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
let progress: HTMLElement;
let scroll = 0;
$: if (progress) {
progress.style.setProperty('--scroll', scroll + '%');
}
onMount(() => {
const updateScroll = () => {
const h = document.documentElement;
const b = document.body;
scroll =
((h.scrollTop || b.scrollTop) / ((h.scrollHeight || b.scrollHeight) - h.clientHeight)) *
100;
};
window.addEventListener('scroll', updateScroll);
updateScroll();
return () => {
window.removeEventListener('scroll', updateScroll);
};
});
</script>
<div
bind:this={progress}
id="progress"
class="top-0 z-20 h-1"
style="background:linear-gradient(to right, hsl(var(--primary)) var(--scroll), transparent 0)"
/>

View file

@ -0,0 +1,86 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { Label, Separator } from 'bits-ui';
import { Textarea } from '$lib/components/ui/textarea/index.js';
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import { toast } from 'svelte-sonner';
let problemDescription = '';
let problemType = 'ui_problem';
const handleSubmit = () => {
console.log({ problemType, problemDescription });
toast.success(`${problemType} submitted`);
problemDescription = '';
problemType = 'ui_problem';
};
</script>
<Dialog.Root>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}>
<div class="flex items-center space-x-2 text-white">
<Icon icon="heroicons:exclamation-triangle-solid" class="size-5" />
<span class="text-sm font-medium">Report a problem</span>
</div>
</Dialog.Trigger>
<form on:submit={handleSubmit}>
<Dialog.Content
class="w-full max-w-[94%] sm:max-w-[490px] md:w-full bg-white dark:bg-zinc-900 flex flex-col"
>
<Dialog.Header>
<Dialog.Title class="text-lg font-semibold tracking-tight">Report a Problem</Dialog.Title>
</Dialog.Header>
<div class="flex-1">
<Dialog.Description class="text-foreground-alt">
<p>
Please describe the problem you're experiencing. We'll look into it as soon as possible.
</p>
</Dialog.Description>
<div class="flex flex-col items-start gap-2 pb-6 pt-7">
<Label.Root class="text-sm font-medium">Problem Type</Label.Root>
<RadioGroup.Root bind:value={problemType} class="space-y-2">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="ui_problem" id="ui-problem" />
<Label.Root for="ui-problem">UI problem</Label.Root>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="article_not_full" id="article-not-full" />
<Label.Root for="article-not-full">Article is not full</Label.Root>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="suggestion" id="suggestion" />
<Label.Root for="suggestion">Suggestion</Label.Root>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="other" id="other" />
<Label.Root for="other">Other</Label.Root>
</div>
</RadioGroup.Root>
</div>
<div class="flex flex-col items-start gap-1 pb-6">
<Label.Root for="problemDescription" class="text-sm font-medium">
Problem Description
</Label.Root>
<Textarea
id="problemDescription"
bind:value={problemDescription}
placeholder="Describe the problem you're experiencing..."
rows={12}
></Textarea>
<p class="mt-2 text-sm text-foreground-alt">
The current opened link will be automatically attached to your report.
</p>
</div>
</div>
<div class="self-end p-4">
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit" on:click={handleSubmit}>Submit</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</form>
</Dialog.Root>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { Button } from '$lib/components/ui/button/index.js';
import { browser } from '$app/environment';
let darkMode = true;
function handleSwitchDarkMode() {
darkMode = !darkMode;
localStorage.setItem('theme', darkMode ? 'dark' : 'light');
darkMode
? document.documentElement.classList.add('dark')
: document.documentElement.classList.remove('dark');
}
if (browser) {
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
darkMode = true;
} else {
document.documentElement.classList.remove('dark');
darkMode = false;
}
}
</script>
<Button
class="px-3 text-gray-600 py-7 dark:text-white hover:text-primary dark:hover:text-primary"
on:click={handleSwitchDarkMode}
variant="ghost"
>
<Icon icon={darkMode ? 'heroicons:moon-solid' : 'heroicons:sun-solid'} class="size-5" />
</Button>

View file

@ -0,0 +1,44 @@
<script lang="ts">
let url = '';
const handleSubmit = (event: Event) => {
event.preventDefault();
window.location.href = `/${url}`;
};
</script>
<div class="w-full p-8 mt-8 bg-white rounded-md shadow-md md:max-w-6xl dark:bg-zinc-800">
<form
on:submit={handleSubmit}
class="flex items-center px-4 py-2 border rounded-md border-zinc-300"
>
<svg
class="w-5 h-5 mr-2 text-zinc-500 dark:text-zinc-100"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
placeholder="Enter Medium post link"
class="w-full bg-transparent border-zinc-300 focus:outline-none text-primary"
autofocus
bind:value={url}
/>
<button
type="submit"
class="px-4 py-2 ml-2 text-white rounded-md bg-primary hover:bg-primary/90 focus:outline-none"
>
Unlock
</button>
</form>
</div>

1
new-web/src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

62
new-web/src/lib/utils.ts Normal file
View file

@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View file

@ -0,0 +1,52 @@
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
type FlyAndScaleParams = {
y?: number;
start?: number;
duration?: number;
};
const defaultFlyAndScaleParams = { y: -8, start: 0.95, duration: 200 };
export function flyAndScale(node: Element, params?: FlyAndScaleParams): TransitionConfig {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const withDefaults = { ...defaultFlyAndScaleParams, ...params };
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return `${str}${key}:${style[key]};`;
}, "");
};
return {
duration: withDefaults.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [withDefaults.y, 0]);
const scale = scaleConversion(t, [0, 1], [withDefaults.start, 1]);
return styleToString({
transform: `${transform} translate3d(0, ${y}px, 0) scale(${scale})`,
opacity: t,
});
},
easing: cubicOut,
};
}

View file

@ -1,48 +1,116 @@
<script lang="ts">
import Header from '$lib/elements/Header.svelte';
import UrlBox from '$lib/elements/UrlBox.svelte';
import Masonry from 'svelte-bricks';
function generateRandomParagraphs(count: number): string[] {
const subjects = [
'Svelte',
'SvelteKit',
'web development',
'JavaScript',
'TypeScript',
'frontend',
'backend',
'full-stack'
];
const verbs = [
'explore',
'learn about',
'discover',
'master',
'understand',
'dive into',
'experiment with',
'practice'
];
const objects = [
'documentation',
'tutorials',
'examples',
'projects',
'community',
'best practices',
'tips and tricks',
'advanced concepts'
];
import HomeBanner from '$lib/elements/HomeBanner.svelte';
import BlogCard from '$lib/elements/BlogCard.svelte';
return Array.from({ length: count }, () => {
const subject = subjects[Math.floor(Math.random() * subjects.length)];
const verb = verbs[Math.floor(Math.random() * verbs.length)];
const object = objects[Math.floor(Math.random() * objects.length)];
return `Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to ${verb} ${subject} ${object} and enhance your development skills.`;
});
}
const blogPosts = [
{
title: '10 Productivity Hacks for Remote Workers',
excerpt: 'Boost your efficiency while working from home with these game-changing strategies.',
imageUrl: 'https://picsum.photos/seed/post1/400/300',
readingTime: '7',
publishedAt: '2023-04-10',
collection: { name: 'Productivity Tips', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Jane Doe',
slug: '10-productivity-hacks-for-remote-workers'
},
{
title: 'The Future of AI in Healthcare',
excerpt:
'Exploring how artificial intelligence is revolutionizing medical diagnoses and treatment plans.',
imageUrl: 'https://picsum.photos/seed/post2/400/300',
readingTime: '10',
publishedAt: '2023-04-12',
collection: { name: 'Tech in Medicine', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'John Smith',
slug: 'the-future-of-ai-in-healthcare'
},
{
title: 'Mastering the Art of Time Management',
excerpt: 'Learn how to take control of your schedule and achieve more in less time.',
imageUrl: 'https://picsum.photos/seed/post3/400/300',
size: 'small',
readingTime: '5',
publishedAt: '2023-04-14',
collection: { name: 'Personal Development', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Emily Johnson',
slug: 'mastering-the-art-of-time-management'
},
{
title: 'Cybersecurity Essentials for Small Businesses',
excerpt: 'Protect your company from digital threats with these crucial security measures.',
imageUrl: 'https://picsum.photos/seed/post4/400/300',
size: 'wide',
readingTime: '8',
publishedAt: '2023-04-16',
collection: { name: 'Business Security', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Michael Brown',
slug: 'cybersecurity-essentials-for-small-businesses'
},
{
title: 'The Psychology of Habit Formation',
excerpt: 'Understand the science behind creating lasting habits and breaking bad ones.',
imageUrl: 'https://picsum.photos/seed/post5/400/300',
size: 'tall',
readingTime: '6',
publishedAt: '2023-04-18',
collection: { name: 'Psychology Insights', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Sarah Wilson',
slug: 'the-psychology-of-habit-formation'
},
{
title: 'Sustainable Tech: Innovations for a Greener Future',
excerpt: 'Discover cutting-edge technologies that are helping to combat climate change.',
bottomImageUrl: 'https://picsum.photos/seed/post6/400/300',
size: 'medium',
readingTime: '9',
publishedAt: '2023-04-20',
collection: { name: 'Green Technology', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'David Lee',
slug: 'sustainable-tech-innovations-for-a-greener-future'
},
{
title: 'Mindfulness in the Digital Age',
excerpt: 'Find balance and reduce stress in an increasingly connected world.',
bottomImageUrl: 'https://picsum.photos/seed/post7/400/300',
size: 'small',
readingTime: '4',
publishedAt: '2023-04-22',
collection: { name: 'Digital Wellness', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Lisa Chen',
slug: 'mindfulness-in-the-digital-age'
},
{
title: 'The Rise of No-Code Development',
excerpt: 'How no-code platforms are democratizing software creation for non-programmers.',
imageUrl: 'https://picsum.photos/seed/post8/400/300',
size: 'medium',
readingTime: '7',
publishedAt: '2023-04-24',
collection: { name: 'Software Trends', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Alex Rodriguez',
slug: 'the-rise-of-no-code-development'
},
{
title: 'The Rise of No-Code Development',
excerpt: 'How no-code platforms are democratizing software creation for non-programmers.',
size: 'medium',
readingTime: '7',
publishedAt: '2023-04-26',
collection: { name: 'Software Trends', avatarId: '1*AZxiin1Cvws3J0TwNUP2sQ.png' },
creator: 'Chris Taylor',
slug: 'the-rise-of-no-code-development-2'
}
];
const randomParagraphs = generateRandomParagraphs(1001);
// Reactive statement to create items array from blogPosts
$: items = blogPosts.map((post, index) => ({ ...post, id: index }));
// Masonry configuration
let [minColWidth, maxColWidth, gap] = [300, 600, 20];
let width, height;
</script>
<head>
@ -50,7 +118,10 @@
</head>
<Header />
<UrlBox />
{#each randomParagraphs as paragraph}
<p>{@html paragraph}</p>
{/each}
<HomeBanner />
<div class="container px-4 py-8 mx-auto">
<Masonry {items} {minColWidth} {maxColWidth} {gap} let:item>
<BlogCard {...item} />
</Masonry>
</div>