- {@render children?.()}
-
-
-
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte
new file mode 100644
index 0000000..66deef7
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+ {@render children?.()}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte
new file mode 100644
index 0000000..b95323f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte
@@ -0,0 +1,30 @@
+
+
+
+ Platform
+
+
+ {#each items as item (item.title)}
+
+
+
+ {item.title}
+
+
+ {/each}
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte
new file mode 100644
index 0000000..93c6c66
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+
+
+ Settings
+
+
+
+ Profile
+
+
+
+
+
+ Sign out
+
+
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte
new file mode 100644
index 0000000..6c72244
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte
@@ -0,0 +1,53 @@
+
+
+
+ {#each cards as card (card.title)}
+
+
+
+
+ {card.title}
+
+
+
+
+
+
{card.value}
+
{card.delta}
+
+
+ {/each}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte
new file mode 100644
index 0000000..6d3e71b
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ {title}
+
+
+
+ {@render children?.()}
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte
new file mode 100644
index 0000000..6e58233
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+{#if href}
+
+ {@render children?.()}
+
+{:else}
+
+{/if}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts
new file mode 100644
index 0000000..39b5e2e
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts
@@ -0,0 +1,15 @@
+import Root, {
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
+ buttonVariants,
+} from './button.svelte';
+
+export {
+ Root,
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
+ buttonVariants,
+ Root as Button,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte
new file mode 100644
index 0000000..318b374
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte
new file mode 100644
index 0000000..aa5d8ef
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte
new file mode 100644
index 0000000..ca1858f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte
new file mode 100644
index 0000000..220f5f5
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte
new file mode 100644
index 0000000..10e3fce
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte
new file mode 100644
index 0000000..e636f70
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte
new file mode 100644
index 0000000..ffaf46b
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts
new file mode 100644
index 0000000..c298ff5
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts
@@ -0,0 +1,24 @@
+import Root from './card.svelte';
+import Header from './card-header.svelte';
+import Title from './card-title.svelte';
+import Description from './card-description.svelte';
+import Action from './card-action.svelte';
+import Content from './card-content.svelte';
+import Footer from './card-footer.svelte';
+
+export {
+ Root,
+ Header,
+ Title,
+ Description,
+ Action,
+ Content,
+ Footer,
+ Root as Card,
+ Header as CardHeader,
+ Title as CardTitle,
+ Description as CardDescription,
+ Action as CardAction,
+ Content as CardContent,
+ Footer as CardFooter,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte
new file mode 100644
index 0000000..130a308
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte
@@ -0,0 +1,35 @@
+
+
+
+ {#snippet children({ checked })}
+
+ {#if checked}
+
+ {/if}
+
+ {/snippet}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts
new file mode 100644
index 0000000..2675e62
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts
@@ -0,0 +1,2 @@
+import Root from './checkbox.svelte';
+export { Root, Root as Checkbox };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte
new file mode 100644
index 0000000..3b65b83
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte
new file mode 100644
index 0000000..d6c8cfb
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+{#if snippet}
+ {@render snippet({ props: ctx as any })}
+{:else if typeof cell === 'string' || typeof cell === 'number' || typeof cell === 'boolean'}
+ {cell}
+{:else if cell}
+ {@html (cell as { html?: string }).html ?? cell}
+{/if}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts
new file mode 100644
index 0000000..236d8ab
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts
@@ -0,0 +1,5 @@
+import { CreateTable } from './create-table.svelte';
+import FlexRender from './flex-render.svelte';
+import { renderCells, renderHeader } from './render-helpers.svelte';
+
+export { CreateTable, FlexRender, renderCells, renderHeader, CreateTable as DataTable };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts
new file mode 100644
index 0000000..c00e6a5
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts
@@ -0,0 +1,26 @@
+import type {
+ CellContext,
+ HeaderContext,
+ RowData,
+} from '@tanstack/table-core';
+import type { Snippet } from 'svelte';
+
+export function renderHeader(
+ ctx: HeaderContext,
+ render: ((ctx: HeaderContext) => unknown) | undefined,
+ snippet?: Snippet<[{ props: Record }]>
+): unknown {
+ if (snippet) return snippet;
+ if (typeof render === 'function') return render(ctx);
+ return render;
+}
+
+export function renderCells(
+ ctx: CellContext,
+ render: ((ctx: CellContext) => unknown) | undefined,
+ snippet?: Snippet<[{ props: Record }]>
+): unknown {
+ if (snippet) return snippet;
+ if (typeof render === 'function') return render(ctx);
+ return render;
+}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts
new file mode 100644
index 0000000..638ec31
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts
@@ -0,0 +1,13 @@
+import Root from 'bits-ui';
+
+export {
+ Root,
+ Root as Dialog,
+ Root.Trigger as DialogTrigger,
+ Root.Portal as DialogPortal,
+ Root.Overlay as DialogOverlay,
+ Root.Content as DialogContent,
+ Root.Title as DialogTitle,
+ Root.Description as DialogDescription,
+ Root.Close as DialogClose,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts
new file mode 100644
index 0000000..725008d
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts
@@ -0,0 +1,20 @@
+import Root from 'bits-ui';
+
+export {
+ Root,
+ Root as DropdownMenu,
+ Root.Trigger as DropdownMenuTrigger,
+ Root.Portal as DropdownMenuPortal,
+ Root.Content as DropdownMenuContent,
+ Root.Arrow as DropdownMenuArrow,
+ Root.Separator as DropdownMenuSeparator,
+ Root.Group as DropdownMenuGroup,
+ Root.GroupHeading as DropdownMenuGroupHeading,
+ Root.Item as DropdownMenuItem,
+ Root.CheckboxItem as DropdownMenuCheckboxItem,
+ Root.RadioGroup as DropdownMenuRadioGroup,
+ Root.RadioItem as DropdownMenuRadioItem,
+ Root.Sub as DropdownMenuSub,
+ Root.SubTrigger as DropdownMenuSubTrigger,
+ Root.SubContent as DropdownMenuSubContent,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte
new file mode 100644
index 0000000..db3cc65
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte
new file mode 100644
index 0000000..aee212d
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte
new file mode 100644
index 0000000..bcface5
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte
new file mode 100644
index 0000000..0c0c421
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte
new file mode 100644
index 0000000..3a6c343
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte
new file mode 100644
index 0000000..8f1137f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte
@@ -0,0 +1,26 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte
new file mode 100644
index 0000000..c10163e
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts
new file mode 100644
index 0000000..07a79a0
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts
@@ -0,0 +1,24 @@
+import Root from 'bits-ui';
+
+import FieldRoot from './field-root.svelte';
+import FieldLabel from './field-label.svelte';
+import FieldDescription from './field-description.svelte';
+import FieldValidation from './field-validation.svelte';
+import FieldControl from './field-control.svelte';
+import FieldLegend from './field-legend.svelte';
+import FieldErrors from './field-errors.svelte';
+
+export {
+ Root,
+ FieldRoot,
+ FieldLabel,
+ FieldDescription,
+ FieldValidation,
+ FieldControl,
+ FieldLegend,
+ FieldErrors,
+ Root as Field,
+ FieldRoot as FieldRoot,
+ Root.Field.Set as FieldSet,
+ Root.Field.Set as FieldSetRoot,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts
new file mode 100644
index 0000000..f1530a2
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts
@@ -0,0 +1,2 @@
+import Root from './input.svelte';
+export { Root, Root as Input };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte
new file mode 100644
index 0000000..723b0b3
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte
@@ -0,0 +1,32 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts
new file mode 100644
index 0000000..9765015
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts
@@ -0,0 +1,2 @@
+import Root from './label.svelte';
+export { Root, Root as Label };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte
new file mode 100644
index 0000000..938c85c
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte
@@ -0,0 +1,20 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts
new file mode 100644
index 0000000..2ac4a4e
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts
@@ -0,0 +1,2 @@
+import Root from './separator.svelte';
+export { Root, Root as Separator };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte
new file mode 100644
index 0000000..9ea8ee9
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte
@@ -0,0 +1,28 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts
new file mode 100644
index 0000000..cfec717
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts
@@ -0,0 +1,13 @@
+import Root from 'bits-ui';
+
+export {
+ Root,
+ Root as Sheet,
+ Root.Trigger as SheetTrigger,
+ Root.Portal as SheetPortal,
+ Root.Overlay as SheetOverlay,
+ Root.Content as SheetContent,
+ Root.Title as SheetTitle,
+ Root.Description as SheetDescription,
+ Root.Close as SheetClose,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts
new file mode 100644
index 0000000..0706a1c
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts
@@ -0,0 +1,3 @@
+export const SIDEBAR_WIDTH = '16rem';
+export const SIDEBAR_WIDTH_ICON = '3rem';
+export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte
new file mode 100644
index 0000000..c877f4f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts
new file mode 100644
index 0000000..db31c4d
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts
@@ -0,0 +1,37 @@
+import { getContext, setContext } from 'svelte';
+import { browser } from '$app/environment';
+import { SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './constants.js';
+
+export const SIDEBAR_ID = Symbol('sidebar');
+
+export type SidebarVariant = 'sidebar' | 'floating' | 'inset';
+export type SidebarCollapsible = 'offcanvas' | 'icon' | 'none';
+
+export type SidebarProps = {
+ variant?: SidebarVariant;
+ collapsible?: SidebarCollapsible;
+ side?: 'left' | 'right';
+};
+
+export type SidebarContextValue = {
+ state: 'expanded' | 'collapsed';
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ variant: SidebarVariant;
+ collapsible: SidebarCollapsible;
+ side: 'left' | 'right';
+ toggleSidebar: () => void;
+};
+
+export function useSidebar() {
+ return getContext(SIDEBAR_ID);
+}
+
+export function setSidebarContext(value: SidebarContextValue) {
+ setContext(SIDEBAR_ID, value);
+}
+
+export { SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte
new file mode 100644
index 0000000..94c3b3d
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte
new file mode 100644
index 0000000..8550ee2
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte
@@ -0,0 +1,25 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte
new file mode 100644
index 0000000..9979ad9
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte
new file mode 100644
index 0000000..cb0c28f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte
@@ -0,0 +1,25 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte
new file mode 100644
index 0000000..a00ec0a
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte
new file mode 100644
index 0000000..5f51f93
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts
new file mode 100644
index 0000000..aa36ec3
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts
@@ -0,0 +1,82 @@
+import { SIDEBAR_ID, setSidebarContext, useSidebar, type SidebarContextValue, type SidebarVariant, type SidebarCollapsible, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './context.svelte.js';
+
+import Provider from './provider.svelte';
+import Root from './root.svelte';
+import Header from './header.svelte';
+import Footer from './footer.svelte';
+import Content from './content.svelte';
+import Group from './group.svelte';
+import GroupLabel from './group-label.svelte';
+import GroupAction from './group-action.svelte';
+import GroupContent from './group-content.svelte';
+import Menu from './menu.svelte';
+import MenuItem from './menu-item.svelte';
+import MenuButton from './menu-button.svelte';
+import MenuAction from './menu-action.svelte';
+import MenuBadge from './menu-badge.svelte';
+import MenuSkeleton from './menu-skeleton.svelte';
+import MenuSub from './menu-sub.svelte';
+import MenuSubItem from './menu-sub-item.svelte';
+import MenuSubButton from './menu-sub-button.svelte';
+import MenuSubTrigger from './menu-sub-trigger.svelte';
+import Rail from './rail.svelte';
+import Separator from './separator.svelte';
+import Input from './input.svelte';
+import Trigger from './trigger.svelte';
+import Inset from './inset.svelte';
+
+export {
+ Provider,
+ Root,
+ Header,
+ Footer,
+ Content,
+ Group,
+ GroupLabel,
+ GroupAction,
+ GroupContent,
+ Menu,
+ MenuItem,
+ MenuButton,
+ MenuAction,
+ MenuBadge,
+ MenuSkeleton,
+ MenuSub,
+ MenuSubItem,
+ MenuSubButton,
+ MenuSubTrigger,
+ Rail,
+ Separator,
+ Input,
+ Trigger,
+ Inset,
+ useSidebar,
+ setSidebarContext,
+ SIDEBAR_ID,
+ SIDEBAR_WIDTH,
+ SIDEBAR_WIDTH_ICON,
+ Root as Sidebar,
+ Provider as SidebarProvider,
+ Header as SidebarHeader,
+ Footer as SidebarFooter,
+ Content as SidebarContent,
+ Group as SidebarGroup,
+ GroupLabel as SidebarGroupLabel,
+ GroupAction as SidebarGroupAction,
+ GroupContent as SidebarGroupContent,
+ Menu as SidebarMenu,
+ MenuItem as SidebarMenuItem,
+ MenuButton as SidebarMenuButton,
+ MenuAction as SidebarMenuAction,
+ MenuBadge as SidebarMenuBadge,
+ MenuSkeleton as SidebarMenuSkeleton,
+ MenuSub as SidebarMenuSub,
+ MenuSubItem as SidebarMenuSubItem,
+ MenuSubButton as SidebarMenuSubButton,
+ MenuSubTrigger as SidebarMenuSubTrigger,
+ Rail as SidebarRail,
+ Separator as SidebarSeparator,
+ Input as SidebarInput,
+ Trigger as SidebarTrigger,
+ Inset as SidebarInset,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte
new file mode 100644
index 0000000..dfa43d2
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte
@@ -0,0 +1,21 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte
new file mode 100644
index 0000000..43fb041
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte
@@ -0,0 +1,28 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte
new file mode 100644
index 0000000..87b5cc1
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte
@@ -0,0 +1,26 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte
new file mode 100644
index 0000000..5bf6da8
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte
@@ -0,0 +1,26 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte
new file mode 100644
index 0000000..abcea33
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte
@@ -0,0 +1,28 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte
new file mode 100644
index 0000000..6639687
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte
new file mode 100644
index 0000000..0b2d871
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte
new file mode 100644
index 0000000..bb4dc58
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte
@@ -0,0 +1,29 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte
new file mode 100644
index 0000000..5645aa4
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte
new file mode 100644
index 0000000..43acdd7
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte
@@ -0,0 +1,25 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte
new file mode 100644
index 0000000..e62f33d
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte
new file mode 100644
index 0000000..f11f3b1
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte
new file mode 100644
index 0000000..f15f24f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte
@@ -0,0 +1,60 @@
+
+
+{@render children?.()}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte
new file mode 100644
index 0000000..6f76476
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte
@@ -0,0 +1,26 @@
+
+
+
+ {children}
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte
new file mode 100644
index 0000000..8e7a032
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte
@@ -0,0 +1,55 @@
+
+
+{#if sidebar?.isMobile}
+
+ {children}
+
+{:else}
+
+ {children}
+
+{/if}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte
new file mode 100644
index 0000000..5f9aa12
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte
@@ -0,0 +1,19 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte
new file mode 100644
index 0000000..45542a2
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte
@@ -0,0 +1,31 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts
new file mode 100644
index 0000000..c3bc460
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts
@@ -0,0 +1,2 @@
+import Root from './skeleton.svelte';
+export { Root, Root as Skeleton };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte
new file mode 100644
index 0000000..16980fc
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts
new file mode 100644
index 0000000..cee8169
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts
@@ -0,0 +1,3 @@
+import { Sonner } from './sonner.svelte';
+
+export { Sonner, Sonner as Toaster };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte
new file mode 100644
index 0000000..3465b61
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts
new file mode 100644
index 0000000..64ed57f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts
@@ -0,0 +1,2 @@
+import Root from './switch.svelte';
+export { Root, Root as Switch };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte
new file mode 100644
index 0000000..f38ed28
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+ {@render children?.()}
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts
new file mode 100644
index 0000000..a998b0e
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts
@@ -0,0 +1,2 @@
+import Root from './textarea.svelte';
+export { Root, Root as Textarea };
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte
new file mode 100644
index 0000000..28ec49d
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte
@@ -0,0 +1,24 @@
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts
new file mode 100644
index 0000000..3e80708
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts
@@ -0,0 +1,9 @@
+import Root from 'bits-ui';
+
+export {
+ Root,
+ Root as Tooltip,
+ Root.Provider as TooltipProvider,
+ Root.Trigger as TooltipTrigger,
+ Root.Content as TooltipContent,
+};
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts
new file mode 100644
index 0000000..f901a2f
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts
@@ -0,0 +1,26 @@
+import { browser } from '$app/environment';
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const isMobile = $state(
+ browser ? window.innerWidth < MOBILE_BREAKPOINT : false
+ );
+
+ if (browser) {
+ const media = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const handler = (event: MediaQueryListEvent) => {
+ isMobile = event.matches;
+ };
+ media.addEventListener('change', handler);
+ $effect(() => {
+ return () => media.removeEventListener('change', handler);
+ });
+ }
+
+ return {
+ get current() {
+ return isMobile;
+ },
+ };
+}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js
deleted file mode 100644
index 21a30f4..0000000
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Counter store — Svelte 5 runes ($state) in a *.svelte.js module.
- * Import the store and use `.count` and `.increment()`.
- */
-
-export function createCounter(initial = 0) {
- let count = $state(initial);
- return {
- get count() {
- return count;
- },
- increment() {
- count += 1;
- },
- decrement() {
- count -= 1;
- },
- reset() {
- count = initial;
- },
- };
-}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js
deleted file mode 100644
index 1b031e6..0000000
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { createCounter } from './counter.svelte.js';
-
-describe('createCounter', () => {
- it('starts at the initial value', () => {
- const c = createCounter(5);
- expect(c.count).toBe(5);
- });
-
- it('increments', () => {
- const c = createCounter(0);
- c.increment();
- expect(c.count).toBe(1);
- });
-
- it('decrements', () => {
- const c = createCounter(3);
- c.decrement();
- expect(c.count).toBe(2);
- });
-
- it('resets to initial', () => {
- const c = createCounter(7);
- c.increment();
- c.increment();
- c.reset();
- expect(c.count).toBe(7);
- });
-});
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts
new file mode 100644
index 0000000..1f58194
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts
@@ -0,0 +1,22 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]): string {
+ return twMerge(clsx(inputs));
+}
+
+export type WithElementRef = T & {
+ ref?: U | null;
+};
+
+export type WithoutChildren = T extends any
+ ? 'children' extends keyof T
+ ? Omit
+ : T
+ : T;
+
+export type WithoutChild = T extends any
+ ? 'child' extends keyof T
+ ? Omit
+ : T
+ : T;
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js
deleted file mode 100644
index b0635aa..0000000
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Misc utility helpers.
- */
-
-export function formatDate(date) {
- return new Date(date).toLocaleDateString();
-}
-
-export function truncate(str, n) {
- return str.length > n ? `${str.slice(0, n)}…` : str;
-}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js
deleted file mode 100644
index de25021..0000000
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { formatDate, truncate } from './format.js';
-
-describe('formatDate', () => {
- it('formats a date string', () => {
- const out = formatDate('2024-01-15');
- expect(typeof out).toBe('string');
- expect(out.length).toBeGreaterThan(0);
- });
-});
-
-describe('truncate', () => {
- it('returns the string when shorter than n', () => {
- expect(truncate('abc', 10)).toBe('abc');
- });
-
- it('truncates with ellipsis when longer', () => {
- expect(truncate('abcdef', 3)).toBe('abc…');
- });
-});
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte
index c142e42..7e45317 100644
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte
@@ -1,9 +1,9 @@
-
- {page.status} — {{ cookiecutter.project_name }}
+ {page.status}
{page.status}
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte
new file mode 100644
index 0000000..74987a1
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+ {@render children?.()}
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts
new file mode 100644
index 0000000..c7ec014
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts
@@ -0,0 +1,5 @@
+import type { LayoutLoad } from './$types';
+
+export function load(): { title: string } {
+ return { title: '{{ cookiecutter.project_name }}' };
+}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js
deleted file mode 100644
index 3d58f98..0000000
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/** @type {import('./$types').PageLoad} */
-export function load() {
- return { title: '{{ cookiecutter.project_name }}' };
-}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte
index 1e34cf2..068c63e 100644
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte
@@ -1,31 +1,62 @@
-
{data.title}
-
-
{{ cookiecutter.description }}
-
+
-
-
Counter: {counter.count}
-
-
-
-
-
-
\ No newline at end of file
+
+
+ Channels
+ Recent channel activity.
+
+
+
+
+
+ {#each columns as col}
+
{col.header as string}
+ {/each}
+
+
+
+ {#each rows as row (row.id)}
+
+
{row.id}
+
{row.name}
+
{row.status}
+
{row.uploaded}
+
+ {/each}
+
+
+
+
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts
new file mode 100644
index 0000000..ee4a97b
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts
@@ -0,0 +1,5 @@
+import type { PageLoad } from './$types';
+
+export function load(): { title: string } {
+ return { title: '{{ cookiecutter.project_name }}' };
+}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js
index 41f6844..123b0d6 100644
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js
@@ -8,7 +8,15 @@ const config = {
adapter: adapter(),
alias: {
$lib: 'src/lib',
- $components: 'src/lib/components',
+ '$lib/*': 'src/lib/*',
+ '$lib/utils': 'src/lib/utils',
+ '$lib/utils/*': 'src/lib/utils/*',
+ '$lib/hooks': 'src/lib/hooks',
+ '$lib/hooks/*': 'src/lib/hooks/*',
+ '$lib/components': 'src/lib/components',
+ '$lib/components/*': 'src/lib/components/*',
+ '$lib/components/ui': 'src/lib/components/ui',
+ '$lib/components/ui/*': 'src/lib/components/ui/*',
},
},
};
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js
deleted file mode 100644
index 2402220..0000000
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-test('homepage shows the project title', async ({ page }) => {
- await page.goto('/');
- await expect(page.locator('h1')).toContainText('{{ cookiecutter.project_name }}');
-});
-
-test('counter increments on click', async ({ page }) => {
- await page.goto('/');
- const buttons = page.getByRole('button');
- await expect(buttons.first()).toBeVisible();
-});
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts
new file mode 100644
index 0000000..0dee708
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts
@@ -0,0 +1,6 @@
+import { expect, test } from '@playwright/test';
+
+test('sidebar visible on dashboard load', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('[data-sidebar="sidebar"]')).toBeVisible();
+});
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json
new file mode 100644
index 0000000..6fa1127
--- /dev/null
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "extends": "@tsconfig/svelte",
+ "compilerOptions": {
+ "moduleResolution": "bundler",
+ "target": "ESNext",
+ "module": "ESNext",
+ "resolveJsonModule": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "verbatimModuleSyntax": false,
+ "baseUrl": ".",
+ "paths": {
+ "$lib": ["src/lib"],
+ "$lib/*": ["src/lib/*"],
+ "$lib/utils": ["src/lib/utils"],
+ "$lib/utils/*": ["src/lib/utils/*"],
+ "$lib/hooks": ["src/lib/hooks"],
+ "$lib/hooks/*": ["src/lib/hooks/*"],
+ "$lib/components": ["src/lib/components"],
+ "$lib/components/*": ["src/lib/components/*"],
+ "$lib/components/ui": ["src/lib/components/ui"],
+ "$lib/components/ui/*": ["src/lib/components/ui/*"]
+ }
+ },
+ "include": [
+ "src/**/*.d.ts",
+ "src/**/*.ts",
+ "src/**/*.js",
+ "src/**/*.svelte",
+ "tests/**/*.ts",
+ "vite.config.ts",
+ "vitest.config.ts",
+ "playwright.config.js"
+ ]
+}
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.ts
similarity index 60%
rename from .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.js
rename to .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.ts
index 822e24e..09bde61 100644
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.js
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.ts
@@ -1,6 +1,7 @@
+import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';
export default defineConfig({
- plugins: [sveltekit()],
+ plugins: [tailwindcss(), sveltekit()],
});
\ No newline at end of file
diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.ts
similarity index 56%
rename from .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.js
rename to .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.ts
index 6b6bc7c..4ef1c32 100644
--- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.js
+++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.ts
@@ -1,12 +1,10 @@
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
-// Separate vitest config — the sveltekit() plugin forces SSR which
-// breaks jsdom-based component tests. Use the plain svelte() plugin.
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
environment: 'jsdom',
- include: ['src/**/*.test.js'],
+ include: ['src/**/*.test.ts'],
},
});
\ No newline at end of file
diff --git a/tests/test_cookiecutter_templates.py b/tests/test_cookiecutter_templates.py
index 0ed94a1..d8d20e5 100644
--- a/tests/test_cookiecutter_templates.py
+++ b/tests/test_cookiecutter_templates.py
@@ -241,19 +241,26 @@ def test_fullstack_structure(render):
assert (render / "backend/src/fs/api/v1/routes/users.py").exists()
assert (render / "backend/src/fs/services/auth_service.py").exists()
assert (render / "backend/migrations").is_dir()
- # frontend has the SvelteKit stack
+ # frontend has the SvelteKit + Tailwind v4 + shadcn-svelte stack (TS)
assert (render / "frontend/package.json").exists()
assert (render / "frontend/svelte.config.js").exists()
- assert (render / "frontend/vite.config.js").exists()
- assert (render / "frontend/vitest.config.js").exists()
+ assert (render / "frontend/vite.config.ts").exists()
+ assert (render / "frontend/vitest.config.ts").exists()
assert (render / "frontend/biome.json").exists()
assert (render / "frontend/knip.json").exists()
- assert (render / "frontend/jsconfig.json").exists()
+ assert (render / "frontend/tsconfig.json").exists()
+ assert (render / "frontend/components.json").exists()
assert (render / "frontend/src/app.html").exists()
+ assert (render / "frontend/src/app.css").exists()
assert (render / "frontend/src/hooks.server.js").exists()
+ assert (render / "frontend/src/routes/+layout.svelte").exists()
assert (render / "frontend/src/routes/+page.svelte").exists()
- assert (render / "frontend/src/lib/stores/counter.svelte.js").exists()
- assert (render / "frontend/tests/e2e/app.spec.js").exists()
+ assert (render / "frontend/src/routes/+page.ts").exists()
+ assert (render / "frontend/src/lib/components/app-sidebar.svelte").exists()
+ assert (render / "frontend/src/lib/utils/cn.svelte.ts").exists()
+ assert (render / "frontend/src/lib/hooks/is-mobile.svelte.ts").exists()
+ assert (render / "frontend/tests/e2e/app.spec.ts").exists()
+ # removed: counter.svelte.js, Header.svelte, jsconfig.json, +page.js
# root CI runs both
assert (render / ".github/workflows/ci.yml").exists()
assert (render / "README.md").exists()
@@ -282,9 +289,10 @@ def test_fullstack_conditional_stripped(render):
@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})])
def test_fullstack_frontend_uses_svelte5(render):
- """package.json pins svelte 5."""
+ """package.json pins svelte 5 (in dependencies or devDependencies)."""
pkg = json.loads((render / "frontend/package.json").read_text())
- svelte_dep = pkg.get("devDependencies", {}).get("svelte", "")
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
+ svelte_dep = deps.get("svelte", "")
assert svelte_dep.startswith("^5.") or svelte_dep.startswith("5"), svelte_dep
@@ -308,9 +316,9 @@ def test_fullstack_error_page_is_fragment(render):
def test_fullstack_page_uses_data_title(render):
"""+page.svelte must consume ``data.title`` from the load function."""
page_svelte = (render / "frontend/src/routes/+page.svelte").read_text()
- page_js = (render / "frontend/src/routes/+page.js").read_text()
+ page_ts = (render / "frontend/src/routes/+page.ts").read_text()
# load returns { title: ... }
- assert "title" in page_js
+ assert "title" in page_ts
# +page.svelte consumes data.title (not a hardcoded cookiecutter literal)
assert "data.title" in page_svelte, "+page.svelte must use data.title from load"
@@ -322,6 +330,76 @@ def test_fullstack_hooks_no_unused_redirect(render):
assert "redirect" not in hooks, "hooks.server.js must not import unused redirect"
+@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})])
+def test_fullstack_has_tailwind_config(render):
+ """vite.config.ts wires @tailwindcss/vite; package.json pins tailwindcss."""
+ vite = (render / "frontend/vite.config.ts").read_text()
+ assert "tailwindcss" in vite, "vite.config.ts must wire @tailwindcss/vite"
+ # tailwindcss() must run BEFORE sveltekit() (order matters)
+ assert vite.index("tailwindcss") < vite.index("sveltekit")
+ pkg = json.loads((render / "frontend/package.json").read_text())
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
+ assert "tailwindcss" in deps, "package.json must depend on tailwindcss"
+ assert "@tailwindcss/vite" in deps, "package.json must depend on @tailwindcss/vite"
+
+
+@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})])
+def test_fullstack_has_shadcn_components_json(render):
+ """components.json configures shadcn-svelte (new-york, neutral, TS)."""
+ cfg_path = render / "frontend/components.json"
+ assert cfg_path.exists(), "components.json must exist"
+ cfg = json.loads(cfg_path.read_text())
+ assert cfg["style"] == "new-york", f"style must be new-york, got {cfg.get('style')}"
+ assert cfg["tailwind"]["baseColor"] == "neutral"
+ assert cfg["typescript"] is True, "typescript must be true"
+ aliases = cfg["aliases"]
+ assert aliases["lib"] == "$lib"
+ assert aliases["utils"] == "$lib/utils"
+ assert aliases["ui"] == "$lib/components/ui"
+ assert aliases["hooks"] == "$lib/hooks"
+
+
+@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})])
+def test_fullstack_layout_has_sidebar_provider(render):
+ """+layout.svelte wires the sidebar around the content (directly or via
+ AppSidebar which embeds Sidebar.Provider)."""
+ layout = (render / "frontend/src/routes/+layout.svelte").read_text()
+ has_provider = "Sidebar.Provider" in layout or "SidebarProvider" in layout
+ has_app_sidebar = "AppSidebar" in layout
+ assert has_provider or has_app_sidebar, (
+ "+layout.svelte must wrap content in Sidebar.Provider (directly or via AppSidebar)"
+ )
+ # AppSidebar itself must contain the Sidebar.Provider
+ app_sidebar = (render / "frontend/src/lib/components/app-sidebar.svelte").read_text()
+ assert "Sidebar.Provider" in app_sidebar or "SidebarProvider" in app_sidebar, (
+ "app-sidebar.svelte must embed Sidebar.Provider"
+ )
+
+
+@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})])
+def test_fullstack_uses_typescript(render):
+ """Frontend is TypeScript: tsconfig.json, no jsconfig.json, +page.ts, lang=ts."""
+ assert (render / "frontend/tsconfig.json").exists(), "tsconfig.json must exist"
+ assert not (render / "frontend/jsconfig.json").exists(), "jsconfig.json must be removed"
+ assert (render / "frontend/src/routes/+page.ts").exists(), "+page.ts must exist"
+ assert not (render / "frontend/src/routes/+page.js").exists(), "+page.js must be removed"
+ page_svelte = (render / "frontend/src/routes/+page.svelte").read_text()
+ assert 'lang="ts"' in page_svelte, '+page.svelte must use