a homebrewed DnD campaign based in the Honkai: Star Rail universe
hsr honkaistarrail dnd
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: /species/new page, /combat page is now filterable (+refactoring) (#44)

authored by

Samantha and committed by
GitHub
199b3b43 427c1332

+906 -447
+1
app/package.json
··· 22 22 "dependencies": { 23 23 "@cloudflare/workers-types": "^4.20251107.0", 24 24 "@lucide/svelte": "^0.553.0", 25 + "@starlight/color": "file:../packages/color", 25 26 "@starlight/icons": "file:../packages/icons", 26 27 "@starlight/tokenizer": "file:../packages/tokenizer", 27 28 "@starlight/types": "file:../packages/types",
+54 -1
app/src/app.css
··· 4 4 --color-hsr-gold: #9F7755; 5 5 --color-hsr-dark: #151512; 6 6 7 - --font-din: 'DIN Pro', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 7 + --font-sans: 'DIN Pro', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 8 8 --font-script: 'Hsr _ Jariloivhertaspacestation', sans-serif; 9 9 --font-serif: 'Fraunces', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; 10 10 } ··· 14 14 background-size: 10px 10px; 15 15 } 16 16 17 + /** typography */ 17 18 @utility small-caps { 18 19 font-variant-caps: small-caps; 19 20 } 21 + 22 + @utility max-w-10ch { 23 + max-width: 10ch; 24 + } 25 + 26 + @utility max-w-15ch { 27 + max-width: 15ch; 28 + } 29 + 30 + @utility max-w-20ch { 31 + max-width: 20ch; 32 + } 33 + 34 + @utility max-w-30ch { 35 + max-width: 30ch; 36 + } 37 + 38 + @utility max-w-40ch { 39 + max-width: 40ch; 40 + } 41 + 42 + @utility max-w-50ch { 43 + max-width: 50ch; 44 + } 45 + 46 + @utility max-w-60ch { 47 + max-width: 60ch; 48 + } 49 + 50 + @utility max-w-70ch { 51 + max-width: 70ch; 52 + } 53 + 54 + @utility max-w-80ch { 55 + max-width: 80ch; 56 + } 57 + 58 + @utility max-w-90ch { 59 + max-width: 90ch; 60 + } 61 + 62 + @utility max-w-100ch { 63 + max-width: 100ch; 64 + } 65 + 66 + @utility max-w-110ch { 67 + max-width: 110ch; 68 + } 69 + 70 + @utility max-w-120ch { 71 + max-width: 120ch; 72 + }
app/src/lib/components/Button.svelte app/src/lib/components/Button/Button.svelte
+22
app/src/lib/components/Button/FormButton.svelte
··· 1 + <script lang="ts"> 2 + import type { SvelteHTMLElements } from 'svelte/elements' 3 + import { tv } from 'tailwind-variants' 4 + 5 + const formButton = tv({ 6 + base: [ 7 + 'cursor-pointer', 8 + 'flex flex-row py-2 px-4', 9 + 'bg-hsr-dark dark:bg-white rounded-md', 10 + 'text-white dark:text-hsr-dark text-center font-medium font-sans', 11 + ] 12 + }) 13 + 14 + type FormButtonType = 'submit' | 'reset' 15 + type FormButtonRootElement = SvelteHTMLElements['input'] 16 + type FormButtonProps = Exclude<FormButtonRootElement, 'type'> & { 17 + type?: FormButtonType 18 + } 19 + let { value, type = 'submit', ...other }: FormButtonProps = $props() 20 + </script> 21 + 22 + <input {...other} class={formButton()} {type} {value} />
+1 -1
app/src/lib/components/Card/Card.svelte
··· 1 1 <script lang="ts"> 2 - import type { ElementAttributes } from '$lib/types' 3 2 import type { WithChildren } from 'bits-ui' 4 3 import { tv } from 'tailwind-variants' 4 + import type { ElementAttributes } from '$lib/types' 5 5 6 6 const card = tv({ 7 7 base: [
+1 -1
app/src/lib/components/Card/CardBody.svelte
··· 9 9 10 10 const cardBody = tv({ 11 11 base: [ 12 - 'text-sm font-din', 12 + 'text-sm font-sans', 13 13 'text-zinc-900 dark:text-white', 14 14 ], 15 15 })
+14 -16
app/src/lib/components/Card/CardFooter.svelte
··· 1 1 <script lang="ts"> 2 2 import type { WithChildren } from 'bits-ui' 3 - import type { SvelteHTMLElements } from 'svelte/elements' 3 + import { tv } from 'tailwind-variants' 4 + import type { ElementAttributes } from '$lib/types' 4 5 import Separator from '$ui/Separator.svelte' 6 + import DescList from '$ui/DescList/DescList.svelte' 5 7 6 8 export type ColumnAmount = 3 | 4 | 5 | 6 7 - export type CardFooterRootElement = SvelteHTMLElements['footer'] 9 + export type CardFooterRootElement = ElementAttributes<'footer'> 8 10 export type CardFooterProps = WithChildren<CardFooterRootElement> & { 9 11 columns?: ColumnAmount, 10 12 } 11 13 let { children, columns = 5, ...other }: CardFooterProps = $props() 12 14 13 - const columnsMap: Record<ColumnAmount, string> = { 14 - 3: 'grid-cols-3', 15 - 4: 'grid-cols-4', 16 - 5: 'grid-cols-5', 17 - 6: 'grid-cols-6', 18 - } 15 + const cardFooter = tv({ 16 + base: [ 17 + 'mt-auto', 18 + 'flex flex-col gap-2', 19 + 'dark:bg-diagonal-lines text-zinc-700/15', 20 + ], 21 + }) 19 22 </script> 20 23 21 - <footer class={[ 22 - "mt-auto", 23 - "flex flex-col gap-4", 24 - "dark:bg-diagonal-lines text-zinc-700/15", 25 - other.class, 26 - ]}> 24 + <footer class={cardFooter({ class: other.class })}> 27 25 <Separator isDecorative /> 28 - <dl class={["grid items-center", columnsMap[columns]]}> 26 + <DescList columns={columns}> 29 27 {@render children?.()} 30 - </dl> 28 + </DescList> 31 29 </footer>
+32
app/src/lib/components/Card/CardHeading.svelte
··· 1 + <script lang="ts"> 2 + import type { WithChildren } from 'bits-ui' 3 + import type { Snippet } from 'svelte' 4 + import type { SvelteHTMLElements } from 'svelte/elements' 5 + import { tv } from 'tailwind-variants' 6 + import Heading from '$ui/Heading/Heading.svelte' 7 + import HeadingGroup from '$ui/Heading/HeadingGroup.svelte' 8 + import SubHeading from '$ui/Heading/SubHeading.svelte' 9 + 10 + export type CardHeadingRootElement = SvelteHTMLElements['header'] 11 + export type CardHeadingProps = WithChildren<CardHeadingRootElement> & { 12 + sideContent?: Snippet, 13 + } 14 + let { children, sideContent, ...other }: CardHeadingProps = $props() 15 + 16 + const cardHeader = tv({ 17 + slots: { 18 + header: 'flex flex-row items-start justify-between', 19 + heading: 'whitespace-nowrap selection:bg-hsr-gold/50 selection:text-hsr-dark', 20 + subHeading: '-mt-1', 21 + }, 22 + }) 23 + const { header, heading, subHeading } = cardHeader() 24 + </script> 25 + 26 + <header {...other} class={header()}> 27 + <HeadingGroup> 28 + <Heading level={3} useEllipsis class={heading()}>{@render children?.()}</Heading> 29 + <SubHeading isScript class={subHeading()}>{@render children?.()}</SubHeading> 30 + </HeadingGroup> 31 + {@render sideContent?.()} 32 + </header>
+9 -8
app/src/lib/components/Checkbox.svelte app/src/lib/components/ChoiceField/Checkbox.svelte
··· 1 1 <script lang="ts"> 2 - import { CheckIcon, MinusIcon } from '@lucide/svelte' 3 - import { Checkbox, Label, useId } from 'bits-ui' 4 - import type { HTMLInputAttributes } from 'svelte/elements' 2 + import CheckIcon from '@lucide/svelte/icons/check' 3 + import MinusIcon from '@lucide/svelte/icons/minus' 4 + import { Checkbox, Label, useId, type CheckboxRootProps } from 'bits-ui' 5 5 import { tv } from 'tailwind-variants' 6 6 7 - type Props = HTMLInputAttributes & { 8 - name: string, 7 + type CheckboxRootElement = CheckboxRootProps 8 + type CheckboxProps = CheckboxRootElement & { 9 + name?: string, 9 10 id?: string, 10 11 label?: string 11 12 indeterminate?: boolean 12 13 } 13 14 14 15 let { 15 - name, 16 16 id = useId(), 17 17 label = '', 18 18 indeterminate = false, 19 - }: Props = $props() 19 + ...other 20 + }: CheckboxProps = $props() 20 21 21 22 const idLabel = $derived(`${id}-label`) 22 23 const styles = tv({ ··· 64 65 65 66 <div class={parent()}> 66 67 <Checkbox.Root 68 + {...other} 67 69 {id} 68 70 aria-labelledby={idLabel} 69 71 class={checkboxRoot()} 70 - name={name} 71 72 {indeterminate} 72 73 > 73 74 {#snippet children({ checked, indeterminate })}
+34
app/src/lib/components/ChoiceField/CheckboxCard.svelte
··· 1 + <script lang="ts"> 2 + import { Label, type CheckboxRootProps } from 'bits-ui' 3 + import { tv } from 'tailwind-variants' 4 + import Checkbox from './Checkbox.svelte' 5 + 6 + type CheckboxCardRootProps = CheckboxRootProps 7 + type CheckboxCardProps = CheckboxCardRootProps & { 8 + name: string, 9 + title: string, 10 + desc: string, 11 + } 12 + 13 + let { name, title, desc }: CheckboxCardProps = $props() 14 + const checkboxCard = tv({ 15 + base: [ 16 + 'cursor-pointer', 17 + 'flex flex-row-reverse gap-2 items-start justify-between py-3 pl-3 pr-1', 18 + 'transition-colors', 19 + 'rounded-lg', 20 + 'border border-zinc-300 dark:border-zinc-800', 21 + 'hover:bg-hsr-gold/5 hover:border-hsr-gold/50', 22 + 'focus-within:bg-hsr-gold/5 focus-within:border-hsr-gold/50', 23 + 'has-checked:bg-hsr-gold/5 has-checked:border-hsr-gold/50', 24 + ] 25 + }) 26 + </script> 27 + 28 + <Label.Root class={checkboxCard()}> 29 + <Checkbox name="ability" /> 30 + <div class="flex flex-col"> 31 + <span>{title}</span> 32 + <div class="text-zinc-500 text-sm">{desc}</div> 33 + </div> 34 + </Label.Root>
+15
app/src/lib/components/ChoiceField/CheckboxGroupLabel.svelte
··· 1 + <script lang="ts"> 2 + import { Checkbox, type CheckboxGroupLabelProps as BitsProps } from 'bits-ui' 3 + import { tv } from 'tailwind-variants' 4 + 5 + const groupLabel = tv({ 6 + base: 'text-hsr-gold uppercase font-sans font-medium', 7 + }) 8 + 9 + type CheckboxGroupLabelProps = BitsProps 10 + let { children, ...other }: CheckboxGroupLabelProps = $props() 11 + </script> 12 + 13 + <Checkbox.GroupLabel {...other} class={groupLabel()}> 14 + {@render children?.()} 15 + </Checkbox.GroupLabel>
+1 -2
app/src/lib/components/Color/ColorPicker.svelte app/src/lib/components/ColorPicker/ColorPicker.svelte
··· 1 1 <script lang="ts"> 2 - import { linearGradient, applyGradient } from './utils' 3 - import type { Number4 } from './utils' 2 + import { linearGradient, applyGradient, type Number4 } from '@starlight/color' 4 3 5 4 // oxlint-disable-next-line no-unassigned-vars 6 5 let canvas: HTMLCanvasElement
+7 -13
app/src/lib/components/Color/utils.ts packages/color/src/index.ts
··· 1 1 export type Number4 = [number, number, number, number] 2 2 export type ColorStop = [number, string] 3 3 export type LinearGradientOptions = { 4 - points: [number, number, number, number], 4 + points: Number4, 5 5 colorStops: ColorStop[], 6 6 } 7 7 8 8 export type ApplyLinearGradientOptions = { 9 9 gradient: CanvasGradient, 10 - fillRect: [number, number, number, number], 10 + fillRect: Number4, 11 11 } 12 12 13 13 export const linearGradient = ( 14 14 context: CanvasRenderingContext2D, 15 15 options: LinearGradientOptions, 16 - ) => { 16 + ): CanvasGradient => { 17 17 const { points, colorStops } = options 18 - const gradient = context.createLinearGradient(points[0], points[1], points[2], points[3]) 18 + const gradient = context.createLinearGradient(...points) 19 19 for (const colorStop of colorStops) { 20 - gradient.addColorStop(colorStop[0], colorStop[1]) 20 + gradient.addColorStop(...colorStop) 21 21 } 22 22 23 23 return gradient ··· 26 26 export const applyGradient = ( 27 27 context: CanvasRenderingContext2D, 28 28 options: ApplyLinearGradientOptions, 29 - ) => { 30 - 29 + ): void => { 31 30 context.fillStyle = options.gradient 32 - context.fillRect( 33 - options.fillRect[0], 34 - options.fillRect[1], 35 - options.fillRect[2], 36 - options.fillRect[3], 37 - ) 31 + context.fillRect(...options.fillRect) 38 32 }
+30
app/src/lib/components/DescList/DescList.svelte
··· 1 + <script lang="ts"> 2 + import type { WithChildren } from 'bits-ui' 3 + import type { SvelteHTMLElements } from 'svelte/elements' 4 + import { tv, type VariantProps } from 'tailwind-variants' 5 + 6 + let { columns = 5, children }: DescriptionListProps = $props() 7 + const dl = tv({ 8 + base: 'grid items-center', 9 + variants: { 10 + columns: { 11 + 3: 'grid-cols-3', 12 + 4: 'grid-cols-4', 13 + 5: 'grid-cols-5', 14 + 6: 'grid-cols-6', 15 + } 16 + } 17 + }) 18 + 19 + type DescriptionListVariants = VariantProps<typeof dl> 20 + type ColumnVariant = DescriptionListVariants['columns'] 21 + 22 + type DescriptionListRootElement = SvelteHTMLElements['dl'] 23 + type DescriptionListProps = WithChildren<DescriptionListRootElement> & { 24 + columns?: ColumnVariant, 25 + } 26 + </script> 27 + 28 + <dl class={dl({ columns: columns })}> 29 + {@render children?.()} 30 + </dl>
+34
app/src/lib/components/DescList/DescListItem.svelte
··· 1 + <script lang="ts"> 2 + import type { WithChildren } from 'bits-ui' 3 + import type { SvelteHTMLElements } from 'svelte/elements' 4 + import { tv } from 'tailwind-variants' 5 + 6 + type DetailRootElement = SvelteHTMLElements['div'] 7 + type Props = WithChildren<DetailRootElement> & { 8 + title: string, 9 + } 10 + 11 + let { title, children }: Props = $props() 12 + const detail = tv({ 13 + slots: { 14 + parent: 'flex flex-col', 15 + dt: [ 16 + 'text-xs dark:text-shadow-sm', 17 + 'text-zinc-900', 18 + 'dark:text-white dark:text-shadow-zinc-700', 19 + 'font-semibold', 20 + 'uppercase tracking-widest', 21 + ], 22 + dd: [ 23 + "text-sm text-zinc-500", 24 + "flex flex-row gap-1 items-center", 25 + ], 26 + }, 27 + }) 28 + const { parent, dt, dd } = detail() 29 + </script> 30 + 31 + <div class={parent()}> 32 + <dt class={dt()}>{title}</dt> 33 + <dd class={dd()}>{@render children?.()}</dd> 34 + </div>
+24
app/src/lib/components/DropdownMenu/DropdownMenuContent.svelte
··· 1 + <script lang="ts"> 2 + import { DropdownMenu } from 'bits-ui' 3 + import type { DropdownMenuContentProps as BitsContentProps, WithChildren } from 'bits-ui' 4 + import { tv } from 'tailwind-variants' 5 + 6 + type DropdownMenuContentRoot = BitsContentProps 7 + type DropdownMenuContentProps = WithChildren<DropdownMenuContentRoot> 8 + let { children }: DropdownMenuContentProps = $props() 9 + 10 + const content = tv({ 11 + base: [ 12 + 'flex flex-col p-2.5 mt-2 w-[230px]', 13 + 'outline-hidden focus-visible:outline-hidden', 14 + 'bg-white dark:bg-hsr-dark', 15 + 'border border-zinc-200 dark:border-zinc-800', 16 + 'rounded-lg shadow-black shadow-2xl', 17 + 'font-sans text-md', 18 + ], 19 + }) 20 + </script> 21 + 22 + <DropdownMenu.Content class={content()}> 23 + {@render children?.()} 24 + </DropdownMenu.Content>
+2 -2
app/src/lib/components/DropdownMenu/DropdownMenuItem.svelte
··· 1 1 <script lang="ts"> 2 - import { DropdownMenu, type DropdownMenuItemProps as BitsDropdownMenuItemProps } from 'bits-ui' 2 + import { DropdownMenu, type DropdownMenuItemProps as BitsItemProps } from 'bits-ui' 3 3 import { tv } from 'tailwind-variants' 4 4 5 5 const menuItem = tv({ ··· 16 16 }, 17 17 }) 18 18 19 - type DropdownMenuItemRootElement = BitsDropdownMenuItemProps 19 + type DropdownMenuItemRootElement = BitsItemProps 20 20 type DropdownMenuItemProps = DropdownMenuItemRootElement & { 21 21 href?: URL | string, 22 22 }
+20
app/src/lib/components/DropdownMenu/DropdownMenuTrigger.svelte
··· 1 + <script lang="ts"> 2 + import { DropdownMenu } from 'bits-ui' 3 + import type { DropdownMenuTriggerProps as BitsTriggerProps, WithChildren } from 'bits-ui' 4 + import { tv } from 'tailwind-variants' 5 + 6 + type DropdownMenuTriggerRoot = BitsTriggerProps 7 + type DropdownMenuTriggerProps = WithChildren<DropdownMenuTriggerRoot> 8 + let { children }: DropdownMenuTriggerProps = $props() 9 + 10 + const trigger = tv({ 11 + base: [ 12 + 'flex flex-row justify-end w-[230px]', 13 + 'outline-hidden focus-visible:outline-hidden', 14 + ] 15 + }) 16 + </script> 17 + 18 + <DropdownMenu.Trigger class={trigger()}> 19 + {@render children?.()} 20 + </DropdownMenu.Trigger>
+6 -6
app/src/lib/components/Heading/Heading.svelte
··· 1 1 <script lang="ts"> 2 2 import type { WithChildren } from 'bits-ui' 3 3 import type { HTMLAttributes } from 'svelte/elements' 4 - import { tv, type VariantProps } from 'tailwind-variants'; 4 + import { tv, type VariantProps } from 'tailwind-variants' 5 5 6 6 const heading = tv({ 7 7 base: 'font-serif text-hsr-gold', 8 8 variants: { 9 9 level: { 10 - 1: 'text-4xl-font-medium', 10 + 1: 'text-3xl font-regular', 11 11 2: 'text-2xl font-light', 12 12 3: 'text-lg font-light', 13 13 }, 14 - withEllipsis: { 14 + useEllipsis: { 15 15 true: 'overflow-hidden text-ellipsis', 16 16 } 17 17 } ··· 23 23 type HeadingRootElement = HTMLAttributes<HTMLHeadingElement> 24 24 type HeadingProps = WithChildren<HeadingRootElement> & { 25 25 level: HeadingLevel, 26 - withEllipsis?: boolean, 26 + useEllipsis?: boolean, 27 27 } 28 28 let { 29 29 children, 30 30 level, 31 - withEllipsis = false, 31 + useEllipsis = false, 32 32 ...other 33 33 }: HeadingProps = $props() 34 34 </script> ··· 36 36 <svelte:element 37 37 this={`h${level}`} 38 38 {...other} 39 - class={heading({level, withEllipsis})} 39 + class={heading({ level, useEllipsis })} 40 40 > 41 41 {@render children?.()} 42 42 </svelte:element>
+8 -4
app/src/lib/components/Heading/HeadingGroup.svelte
··· 1 1 <script lang="ts"> 2 2 import type { WithChildren } from 'bits-ui' 3 - import type { SvelteHTMLElements } from 'svelte/elements' 4 - import { cn } from 'tailwind-variants' 3 + import { cn, tv } from 'tailwind-variants' 4 + import type { ElementAttributes } from '$lib/types' 5 5 6 - type HeadingGroupRootElement = SvelteHTMLElements['hgroup'] 6 + type HeadingGroupRootElement = ElementAttributes<'hgroup'> 7 7 type HeadingGroupProps = WithChildren<HeadingGroupRootElement> 8 8 let { children, ...other }: HeadingGroupProps = $props() 9 + 10 + const headingGroup = tv({ 11 + base: 'leading-4', 12 + }) 9 13 </script> 10 14 11 - <hgroup {...other} class={cn('leading-relaxed', other.class)}> 15 + <hgroup {...other} class={headingGroup({ class: other.class })}> 12 16 {@render children?.()} 13 17 </hgroup>
+1 -1
app/src/lib/components/Profile.svelte
··· 1 1 <script lang="ts"> 2 2 import type { SvelteHTMLElements } from 'svelte/elements' 3 + import { tv } from 'tailwind-variants' 3 4 import ProfilePicture from './ProfilePicture.svelte' 4 - import { tv } from 'tailwind-variants'; 5 5 6 6 type ProfileRootElement = SvelteHTMLElements['div'] 7 7 type ProfileProps = ProfileRootElement & {
+6 -4
app/src/lib/components/Separator.svelte
··· 1 1 <script lang="ts"> 2 + import { Separator } from 'bits-ui' 3 + import { tv } from 'tailwind-variants' 2 4 import type { ElementAttributes } from '$lib/types' 3 - import { tv } from 'tailwind-variants' 4 5 5 6 const separator = tv({ 6 7 slots: { ··· 53 54 }: SeparatorProps = $props() 54 55 </script> 55 56 56 - <div 57 + <Separator.Root 58 + decorative={isDecorative} 57 59 role={isDecorative ? 'none' : 'separator'} 58 - aria-orientation={!isDecorative ? 'horizontal' : undefined} 60 + orientation={!isDecorative ? 'horizontal' : undefined} 59 61 class={root({class: props.class})} 60 62 > 61 63 <div class={outer({isSemiDashed})}></div> ··· 63 65 <div class={middle()}></div> 64 66 <div class={circle()}></div> 65 67 <div class={outer({isSemiDashed})}></div> 66 - </div> 68 + </Separator.Root>
+5 -5
app/src/lib/components/Site/Header.svelte
··· 12 12 header: [ 13 13 'flex flex-row justify-between items-center', 14 14 'py-2 px-32', 15 - 'font-din', 15 + 'font-sans', 16 16 'border-b border-b-zinc-200 dark:border-zinc-800', 17 17 ], 18 18 logo: [ 19 - "px-2 -ml-2", 20 - "group flex flex-row items-center gap-2", 21 - "font-din font-light", 22 - "text-zinc-700 text-base uppercase leading-5", 19 + 'px-2 -ml-2', 20 + 'group flex flex-row items-center gap-2', 21 + 'font-sans font-light', 22 + 'text-zinc-700 text-base uppercase leading-5', 23 23 ] 24 24 } 25 25 })
+2 -2
app/src/lib/components/Site/PageLayout.svelte
··· 1 1 <script lang="ts"> 2 - import type { ElementAttributes } from '$lib/types' 3 2 import type { WithChildren } from 'bits-ui' 4 3 import { tv, type VariantProps } from 'tailwind-variants' 4 + import type { ElementAttributes } from '$lib/types' 5 5 6 6 const pageLayout = tv({ 7 - base: 'py-4 px-32 font-din', 7 + base: 'py-4 px-32 font-sans', 8 8 variants: { 9 9 display: { 10 10 flex: 'flex',
+1 -1
app/src/lib/components/Text/CopyText.svelte
··· 1 1 <script lang="ts"> 2 - import { CopyIcon } from '@lucide/svelte' 2 + import CopyIcon from '@lucide/svelte/icons/x' 3 3 import type { SvelteHTMLElements } from 'svelte/elements' 4 4 5 5 type CopyTextRootElement = SvelteHTMLElements['span']
-31
app/src/lib/components/Text/Detail.svelte
··· 1 - <script lang="ts"> 2 - import type { WithChildren } from 'bits-ui' 3 - import type { Snippet } from 'svelte' 4 - import type { SvelteHTMLElements } from 'svelte/elements' 5 - 6 - type DetailRootElement = SvelteHTMLElements['div'] 7 - type Props = WithChildren<DetailRootElement> & { 8 - title: string, 9 - children?: Snippet, 10 - } 11 - 12 - let { title, children }: Props = $props() 13 - </script> 14 - 15 - <div class="flex flex-col"> 16 - <dt class={[ 17 - "text-xs dark:text-shadow-sm", 18 - "text-zinc-900", 19 - "dark:text-white dark:text-shadow-zinc-700", 20 - "font-semibold", 21 - "uppercase tracking-widest", 22 - ]}> 23 - {title} 24 - </dt> 25 - <dd class={[ 26 - "text-sm text-zinc-500", 27 - "flex flex-row gap-1 items-center", 28 - ]}> 29 - {@render children?.()} 30 - </dd> 31 - </div>
+7 -4
app/src/lib/components/Text/Text.svelte
··· 1 1 <script lang="ts"> 2 2 import type { WithChildren } from 'bits-ui' 3 - import { tv } from 'tailwind-variants' 4 - import type { Color, ElementAttributes } from '$lib/types.ts' 3 + import { tv, type VariantProps } from 'tailwind-variants' 4 + import type { ElementAttributes } from '$lib/types' 5 5 6 6 const text = tv({ 7 7 variants: { ··· 32 32 } 33 33 }) 34 34 35 + type TextVariants = VariantProps<typeof text> 36 + type ColorVariant = TextVariants['color'] 37 + 35 38 type TextRootElement = ElementAttributes<'span'> 36 - export type TextProps = WithChildren<TextRootElement> & { 39 + type TextProps = WithChildren<TextRootElement> & { 37 40 bold?: boolean, 38 41 underline?: boolean, 39 - color?: Color, 42 + color?: ColorVariant, 40 43 } 41 44 42 45 let {
+13
app/src/lib/components/TextField/CharacterCounter.svelte
··· 1 + <script lang="ts"> 2 + import type { SvelteHTMLElements } from 'svelte/elements' 3 + 4 + type CharacterCounterRootElement = SvelteHTMLElements['div'] 5 + type CharacterCounterProps = CharacterCounterRootElement & { 6 + current: number, 7 + max: number, 8 + } 9 + 10 + let { current, max }: CharacterCounterProps = $props() 11 + </script> 12 + 13 + <div class="text-zinc-700">{current}/{max}</div>
+34
app/src/lib/components/TextField/ClearButton.svelte
··· 1 + 2 + <script lang="ts"> 3 + import XIcon from '@lucide/svelte/icons/x' 4 + import type { SvelteHTMLElements } from 'svelte/elements' 5 + import { tv } from 'tailwind-variants' 6 + 7 + type ClearButtonRootElement = SvelteHTMLElements['button'] 8 + type ClearButtonProps = ClearButtonRootElement 9 + let { ...other }: ClearButtonProps = $props() 10 + 11 + const clearButton = tv({ 12 + slots: { 13 + button: [ 14 + 'rounded-full p-1 cursor-pointer', 15 + 'transition-colors', 16 + 'bg-zinc-200 dark:bg-zinc-700', 17 + 'hover:bg-hsr-gold', 18 + ], 19 + icon: [ 20 + 'size-3 stroke-zinc-400 dark:stroke-hsr-dark', 21 + 'hover:stroke-white', 22 + ], 23 + } 24 + }) 25 + const { button, icon } = clearButton() 26 + </script> 27 + 28 + <button 29 + {...other} 30 + title="Clear text field" 31 + class={button()} 32 + > 33 + <XIcon class={icon()} /> 34 + </button>
+25
app/src/lib/components/TextField/HelpText.svelte
··· 1 + <script lang="ts"> 2 + import type { WithChildren } from 'bits-ui' 3 + import type { SvelteHTMLElements } from 'svelte/elements' 4 + import { tv } from 'tailwind-variants' 5 + 6 + type HelpTextRootElement = SvelteHTMLElements['div'] 7 + type HelpTextProps = WithChildren<HelpTextRootElement> & { 8 + isValid: boolean, 9 + } 10 + let { children, isValid = true, ...other }: HelpTextProps = $props() 11 + 12 + const helpText = tv({ 13 + base: 'text-sm', 14 + variants: { 15 + isValid: { 16 + true: 'text-zinc-500', 17 + false: 'text-red-500', 18 + } 19 + } 20 + }) 21 + </script> 22 + 23 + <div {...other} class={helpText({isValid})}> 24 + {@render children?.()} 25 + </div>
+74
app/src/lib/components/TextField/MultiTextField.svelte
··· 1 + <script lang="ts"> 2 + import { Label } from 'bits-ui' 3 + import type { SvelteHTMLElements } from 'svelte/elements' 4 + import { tv } from 'tailwind-variants' 5 + import HelpText from './HelpText.svelte' 6 + import CharacterCounter from './CharacterCounter.svelte' 7 + 8 + type MultiTextFieldRootElement = SvelteHTMLElements['textarea'] 9 + type MultiTextFieldProps = Exclude<MultiTextFieldRootElement, 'resize'> & { 10 + label: string, 11 + name: string, 12 + help?: string, 13 + resizable?: boolean, 14 + } 15 + 16 + let { 17 + label, 18 + name, 19 + placeholder, 20 + help, 21 + resizable = true, 22 + maxlength, 23 + ...other 24 + }: MultiTextFieldProps = $props() 25 + let value = $state('') 26 + 27 + const multi = tv({ 28 + slots: { 29 + root: [ 30 + 'flex flex-col gap-0.75', 31 + 'w-full', 32 + ], 33 + textarea: [ 34 + 'appearance-none', 35 + 'p-2', 36 + 'bg-white dark:bg-hsr-dark', 37 + 'border border-zinc-300 dark:border-zinc-800', 38 + 'placeholder:text-zinc-700', 39 + 'transition-colors', 40 + 'hover:border-hsr-gold/50', 41 + 'focus:outline-1 focus:outline-hsr-gold', 42 + 'focus-within:border-hsr-gold', 43 + 'focus-within:hover:border-hsr-gold', 44 + 'focus-within:shadow-2xs', 45 + 'focus-within:drop-shadow-hsr-gold', 46 + ], 47 + }, 48 + variants: { 49 + resizable: { 50 + true: 'resize-y', 51 + false: 'resize-none', 52 + } 53 + } 54 + }) 55 + const { root, textarea } = multi() 56 + </script> 57 + 58 + <div class={root()}> 59 + <div class="flex flex-row justify-between"> 60 + <Label.Root class="font-medium" for={name}>{label}</Label.Root> 61 + {#if maxlength} 62 + <CharacterCounter current={value.length} max={maxlength} /> 63 + {/if} 64 + </div> 65 + <textarea 66 + {...other} 67 + {name} 68 + {placeholder} 69 + class={textarea()} 70 + ></textarea> 71 + {#if help} 72 + <HelpText isValid>{help}</HelpText> 73 + {/if} 74 + </div>
+88 -29
app/src/lib/components/TextField/TextField.svelte
··· 1 1 <script lang="ts"> 2 - import type { ElementAttributes } from '$lib/types' 3 2 import { Label } from 'bits-ui' 3 + import type { Snippet } from 'svelte' 4 + import type { SvelteHTMLElements } from 'svelte/elements' 4 5 import { tv } from 'tailwind-variants' 6 + import HelpText from './HelpText.svelte' 7 + import CharacterCounter from './CharacterCounter.svelte' 8 + import ClearButton from './ClearButton.svelte' 5 9 6 - const textField = tv({ 7 - slots: { 8 - fieldView: [ 9 - 'flex flex-col', 10 - 'bg-hsr-dark border border-zinc-800 p-2 rounded-md', 11 - 'transition-colors', 12 - 'group', 13 - 'focus-within:border-hsr-gold', 14 - ], 15 - labelView: [ 16 - 'text-xs text-zinc-500 uppercase', 17 - 'transition-colors', 18 - 'group-focus-within:text-hsr-gol', 19 - ], 20 - inputView: [ 21 - 'focus:outline-none', 22 - ], 23 - } 24 - }) 25 - const { fieldView, labelView, inputView } = textField() 10 + type TextFieldInputType = 11 + | 'text' 12 + | 'email' 13 + | 'url' 14 + | 'number' 26 15 27 - type TextFieldElement = ElementAttributes<'input'> 28 - type TextFieldProps = TextFieldElement & { 16 + type TextFieldRootElement = SvelteHTMLElements['input'] 17 + type TextFieldProps = TextFieldRootElement & { 18 + type?: TextFieldInputType, 29 19 label: string, 30 20 name: string, 21 + clearable?: boolean, 22 + helpMessage?: string, 23 + before?: Snippet, 24 + after?: Snippet, 31 25 } 26 + 32 27 let { 33 - name, 28 + type = 'text', 34 29 label, 35 - type = 'text', 30 + name, 36 31 placeholder, 37 - ...props 32 + clearable = true, 33 + helpMessage, 34 + before, 35 + after, 36 + maxlength, 37 + ...other 38 38 }: TextFieldProps = $props() 39 + 40 + let value = $state('') 41 + let isEmpty = $derived(value === '') 42 + function clearField() { 43 + value = '' 44 + } 45 + 46 + const field = tv({ 47 + slots: { 48 + root: [ 49 + 'flex flex-col gap-0.75', 50 + 'max-w-40ch', 51 + ], 52 + inputParent: [ 53 + 'flex flex-row items-center gap-1', 54 + 'p-2', 55 + 'w-full', 56 + 'bg-white dark:bg-hsr-dark', 57 + 'border border-zinc-300 dark:border-zinc-800', 58 + 'transition-colors', 59 + 'hover:border-hsr-gold/50', 60 + 'focus-within:outline-1', 61 + 'focus-within:outline-hsr-gold', 62 + 'focus-within:border-hsr-gold', 63 + 'focus-within:hover:border-hsr-gold', 64 + 'focus-within:shadow-2xs', 65 + 'focus-within:drop-shadow-hsr-gold', 66 + ], 67 + inputElement: [ 68 + 'appearance-none', 69 + 'w-full', 70 + 'focus:outline-none', 71 + 'placeholder:text-zinc-700', 72 + ], 73 + }, 74 + }) 75 + const { root, inputParent, inputElement } = field() 39 76 </script> 40 77 41 - <div class={fieldView({class: props.class})}> 42 - <Label.Root for={name} class={labelView()}>{label}</Label.Root> 43 - <input {...props} class={inputView()} {name} {type} {placeholder} /> 78 + <div class={root()}> 79 + <div class="flex flex-row justify-between"> 80 + <Label.Root class="font-medium" for={name}>{label}</Label.Root> 81 + {#if maxlength} 82 + <CharacterCounter current={value.length} max={maxlength} /> 83 + {/if} 84 + </div> 85 + <div class={inputParent()}> 86 + {@render before?.()} 87 + <input 88 + {...other} 89 + bind:value 90 + type={type} 91 + class={inputElement()} 92 + {placeholder} 93 + {name} 94 + /> 95 + {#if clearable && !isEmpty} 96 + <ClearButton onclick={() => clearField()} /> 97 + {/if} 98 + {@render after?.()} 99 + </div> 100 + {#if helpMessage} 101 + <HelpText isValid>{helpMessage}</HelpText> 102 + {/if} 44 103 </div>
-107
app/src/lib/components/UserRecordRow.svelte
··· 1 - <script lang="ts"> 2 - import { FlameIcon, PlusIcon, ScrollIcon } from '@lucide/svelte' 3 - import type { ClassValue } from 'svelte/elements' 4 - 5 - import type { CharacterLevel } from '@starlight/types/dnd' 6 - import type { Color } from '$lib/types.ts' 7 - import Checkbox from '$ui/Checkbox.svelte' 8 - import Chip from '$ui/Chip/Chip.svelte' 9 - import CopyText from '$ui/Text/CopyText.svelte' 10 - 11 - type Role = 'DM' | 'Player' 12 - type CharacterRecord = { 13 - name: string, 14 - level: CharacterLevel, 15 - species: string, 16 - } 17 - 18 - export type UserRecord = { 19 - name: string, 20 - username: string, 21 - character?: CharacterRecord, 22 - joined: string, 23 - role: Role, 24 - color: Color, 25 - } 26 - type UserRecordRowProps = { 27 - user: UserRecord, 28 - } 29 - 30 - let { user }: UserRecordRowProps = $props() 31 - const rowStyles: ClassValue = ['py-1'] 32 - const colorLabel: Record<Color, string> = { 33 - white: 'White', 34 - red: 'Red', 35 - cyan: 'Cyan', 36 - purple: 'Purple', 37 - green: 'Green', 38 - indigo: 'Indigo', 39 - yellow: 'Yellow', 40 - gold: 'Gold', 41 - blue: 'Blue', 42 - fuchsia: 'Fuchsia', 43 - } 44 - </script> 45 - 46 - {#snippet character(character?: CharacterRecord)} 47 - {#if user.character} 48 - <div class="flex flex-col grow"> 49 - <span class="font-medium">{user.character.name}</span> 50 - <span class="text-zinc-400">Lvl {user.character.level} {user.character.species}</span> 51 - </div> 52 - {:else} 53 - <div class="text-zinc-400">N/A</div> 54 - {/if} 55 - {/snippet} 56 - 57 - {#snippet role(role: Role)} 58 - {#if role === 'DM'} 59 - <Chip style="fill" size="sm" color="yellow"> 60 - <ScrollIcon size="16" stroke-width={2} /> 61 - The Narrator 62 - </Chip> 63 - {:else} 64 - <Chip style="fill" size="sm" color="white"> 65 - <FlameIcon size="16" class="stroke-hsr-dark fill-hsr-dark" /> 66 - Storyteller 67 - </Chip> 68 - {/if} 69 - {/snippet} 70 - 71 - <tr class="hover:bg-zinc-800 transition-colors"> 72 - <td class={[rowStyles, "py-2 text-center pl-6"]}><Checkbox /></td> 73 - <td class={[rowStyles]}> 74 - <div class="flex flex-col grow"> 75 - <CopyText class="font-medium" text={user.name} /> 76 - <span class="text-zinc-400">{user.username}</span> 77 - </div> 78 - </td> 79 - <td class={[rowStyles]}> 80 - {@render character(user.character)} 81 - </td> 82 - <td class={[rowStyles]}>{user.joined}</td> 83 - <td class={[rowStyles]}> 84 - <div class="flex flex-row gap-2"> 85 - {@render role(user.role)} 86 - <div class={[ 87 - "group p-2 cursor-pointer", 88 - "bg-zinc-700 border-2 border-zinc-700 rounded-full", 89 - "transition-colors", 90 - "hover:bg-zinc-800 hover:border-zinc-500", 91 - ]}> 92 - <PlusIcon size="16" /> 93 - </div> 94 - </div> 95 - </td> 96 - <td class={[rowStyles]}> 97 - <Chip color={user.color} size="sm" style="outline" withDot> 98 - {colorLabel[user.color]} 99 - </Chip> 100 - </td> 101 - </tr> 102 - 103 - <style> 104 - td:not(:first-child) { 105 - padding-right: 2rem; 106 - } 107 - </style>
+9 -1
app/src/lib/hsr/elements.ts
··· 6 6 import { UniverseIcon, WhirlIcon } from '@starlight/icons' 7 7 import type { Element } from '@starlight/types/hsr' 8 8 import type { Component } from 'svelte' 9 - import type { ElementColor } from '../types' 9 + 10 + export type ElementColor = 11 + | 'red' 12 + | 'white' 13 + | 'yellow' 14 + | 'green' 15 + | 'cyan' 16 + | 'indigo' 17 + | 'purple' 10 18 11 19 type ElementRecord = { 12 20 text: string,
+49
app/src/lib/patterns/AbilityCard.svelte
··· 1 + <script lang="ts"> 2 + import type { DiceNotation, HitDc, SpellComponent } from '@starlight/types/dnd' 3 + import type { Element, Mechanic } from '@starlight/types/hsr' 4 + import CombatText from '$patterns/CombatText.svelte' 5 + import ElementText from '$patterns/Element/ElementText.svelte' 6 + import MechanicChip from '$patterns/Mechanic/MechanicChip.svelte' 7 + import Card, { type CardProps } from '$ui/Card/Card.svelte' 8 + import CardBody from '$ui/Card/CardBody.svelte' 9 + import CardFooter from '$ui/Card/CardFooter.svelte' 10 + import CardHeading from '$ui/Card/CardHeading.svelte' 11 + import DescListItem from '$ui/DescList/DescListItem.svelte' 12 + 13 + export type AbilityCardProps = CardProps & { 14 + name: string, 15 + desc: string, 16 + mechanic: Mechanic, 17 + details: { 18 + range: string, 19 + hit_dc: HitDc, 20 + damage: DiceNotation, 21 + element: Element, 22 + components?: SpellComponent[], 23 + } 24 + } 25 + let { name, desc, mechanic, details, ...other }: AbilityCardProps = $props() 26 + </script> 27 + 28 + {#snippet mechanicView()} 29 + <MechanicChip mechanic={mechanic} style="outline" size="sm" /> 30 + {/snippet} 31 + 32 + <Card {...other}> 33 + <CardHeading sideContent={mechanicView}> 34 + {name} 35 + </CardHeading> 36 + <CardBody> 37 + <CombatText text={desc} /> 38 + </CardBody> 39 + <CardFooter> 40 + <DescListItem title="Element"><ElementText element={details.element} /></DescListItem> 41 + <DescListItem title="Range">{details.range}</DescListItem> 42 + <DescListItem title="Hit/DC">{details.hit_dc}</DescListItem> 43 + <DescListItem title="Damage">{details.damage}</DescListItem> 44 + {#if details.components} 45 + {@const componentText = details.components.join(', ')} 46 + <DescListItem title="Components">{componentText}</DescListItem> 47 + {/if} 48 + </CardFooter> 49 + </Card>
-43
app/src/lib/patterns/AbilityCard/AbilityCard.svelte
··· 1 - <script lang="ts"> 2 - import type { DiceNotation, HitDc } from '@starlight/types/dnd' 3 - import type { Element, Mechanic } from '@starlight/types/hsr' 4 - import CombatText from '$patterns/CombatText.svelte' 5 - import ElementText from '$patterns/Element/ElementText.svelte' 6 - import Card, { type CardProps } from '$ui/Card/Card.svelte' 7 - import CardBody from '$ui/Card/CardBody.svelte' 8 - import CardFooter from '$ui/Card/CardFooter.svelte' 9 - import Detail from '$ui/Text/Detail.svelte' 10 - import AbilityCardHeading from './AbilityCardHeading.svelte' 11 - 12 - export type AbilityCardProps = CardProps & { 13 - name: string, 14 - desc: string, 15 - mechanic: Mechanic, 16 - details?: { 17 - range: string, 18 - hit_dc: HitDc, 19 - damage: DiceNotation, 20 - element: Element, 21 - components?: ('V'|'S'|'M')[], 22 - } 23 - } 24 - let { name, desc, mechanic, details, ...other }: AbilityCardProps = $props() 25 - </script> 26 - 27 - <Card {...other}> 28 - <AbilityCardHeading mechanic={mechanic}>{name}</AbilityCardHeading> 29 - <CardBody> 30 - <CombatText text={desc} /> 31 - </CardBody> 32 - {#if details} 33 - <CardFooter> 34 - <Detail title="Range">{details.range}</Detail> 35 - <Detail title="Hit/DC">{details.hit_dc}</Detail> 36 - <Detail title="Damage">{details.damage}</Detail> 37 - <Detail title="Element"><ElementText element={details.element} /></Detail> 38 - {#if details.components} 39 - <Detail title="Components">{details.components.join(', ')}</Detail> 40 - {/if} 41 - </CardFooter> 42 - {/if} 43 - </Card>
-25
app/src/lib/patterns/AbilityCard/AbilityCardHeading.svelte
··· 1 - <script lang="ts"> 2 - import type { Mechanic } from '@starlight/types/hsr' 3 - import type { WithChildren } from 'bits-ui' 4 - import type { SvelteHTMLElements } from 'svelte/elements' 5 - import Heading from '$ui/Heading/Heading.svelte' 6 - import HeadingGroup from '$ui/Heading/HeadingGroup.svelte' 7 - import SubHeading from '$ui/Heading/SubHeading.svelte' 8 - import MechanicChip from '$patterns/Mechanic/MechanicChip.svelte' 9 - 10 - export type AbilityCardHeadingRootElement = SvelteHTMLElements['header'] 11 - export type AbilityCardHeadingProps = WithChildren<AbilityCardHeadingRootElement> & { 12 - mechanic: Mechanic, 13 - } 14 - let { children, mechanic, ...other }: AbilityCardHeadingProps = $props() 15 - </script> 16 - 17 - <header {...other} class="flex flex-row items-center justify-between"> 18 - <HeadingGroup> 19 - <Heading level={3} withEllipsis class={["whitespace-nowrap selection:bg-hsr-gold/50 selection:text-hsr-dark"]}> 20 - {@render children?.()} 21 - </Heading> 22 - <SubHeading isScript class="-mt-1">{@render children?.()}</SubHeading> 23 - </HeadingGroup> 24 - <MechanicChip mechanic={mechanic} style="outline" size="sm" /> 25 - </header>
-2
app/src/lib/patterns/AbilityCard/exports.ts
··· 1 - export { default as Heading } from './AbilityCardHeading.svelte' 2 - export type { AbilityCardHeadingProps as HeadingProps } from './AbilityCardHeading.svelte'
-1
app/src/lib/patterns/AbilityCard/index.ts
··· 1 - export * as AbilityCard from './exports'
+1 -1
app/src/lib/patterns/BoostedAbility.svelte
··· 1 1 <script lang="ts"> 2 - import { ChevronsUpIcon } from '@lucide/svelte' 2 + import ChevronsUpIcon from '@lucide/svelte/icons/chevrons-up' 3 3 import type { AbilityMod } from '@starlight/types/dnd' 4 4 import type { SvelteHTMLElements } from 'svelte/elements' 5 5
+4 -7
app/src/lib/patterns/Mechanic/MechanicChip.svelte
··· 1 1 <script lang="ts"> 2 2 import { mechanicLabel } from '$lib/hsr/mechanic' 3 3 import type { Mechanic } from '@starlight/types/hsr' 4 - import type { Size, Style } from '$lib/types.ts' 5 4 import { Chip, type ChipProps } from '$ui/Chip' 6 5 import MechanicIcon from './MechanicIcon.svelte' 7 6 7 + type Style = 'fill' | 'outline' 8 + type Size = 'md' | 'sm' 8 9 type StyleRecord = { 9 10 iconColor: 'gold' | 'dark' 10 11 } ··· 18 19 19 20 let { style, size = 'md', mechanic, ...props }: MechanicChipProps = $props() 20 21 const styleMap: Record<Style, StyleRecord> = { 21 - fill: { 22 - iconColor: 'dark', 23 - }, 24 - outline: { 25 - iconColor: 'gold', 26 - }, 22 + fill: { iconColor: 'dark' }, 23 + outline: { iconColor: 'gold' }, 27 24 } 28 25 </script> 29 26
+1 -1
app/src/lib/patterns/SpeciesCard.svelte
··· 1 1 <script lang="ts"> 2 - import { DraftingCompassIcon } from '@lucide/svelte' 2 + import DraftingCompassIcon from '@lucide/svelte/icons/drafting-compass' 3 3 import type { WithChildren } from 'bits-ui' 4 4 import type { SvelteHTMLElements } from 'svelte/elements' 5 5 import Card from '$ui/Card/Card.svelte'
-30
app/src/lib/types.ts
··· 11 11 Omit<SvelteHTMLElements[T], 'class'> & { 12 12 class?: ClassNameValue | undefined | null 13 13 } 14 - 15 - /* ui component types */ 16 - export type ElementColor = 17 - | 'red' 18 - | 'white' 19 - | 'yellow' 20 - | 'green' 21 - | 'cyan' 22 - | 'indigo' 23 - | 'purple' 24 - 25 - export type Color = 26 - | 'gold' 27 - | 'white' 28 - | 'red' 29 - | 'yellow' 30 - | 'green' 31 - | 'cyan' 32 - | 'blue' 33 - | 'indigo' 34 - | 'purple' 35 - | 'fuchsia' 36 - 37 - export type Style = 38 - | 'fill' 39 - | 'outline' 40 - 41 - export type Size = 42 - | 'md' 43 - | 'sm'
+3 -3
app/src/params/species.ts
··· 1 - import { SpeciesArray, type Species } from '@starlight/types/hsr' 1 + import { SpeciesEnum, type Species } from '@starlight/types/hsr' 2 2 import type { ParamMatcher } from '@sveltejs/kit' 3 3 4 - const set = new Set(SpeciesArray) as Set<string> 5 - export const match: ParamMatcher = (p): p is Species => set.has(p) 4 + export const match: ParamMatcher = (p): p is Species => 5 + SpeciesEnum.safeParse(p).success
+40 -46
app/src/routes/+layout.svelte
··· 3 3 import { DropdownMenu, type WithChildren } from 'bits-ui' 4 4 import { onMount } from 'svelte' 5 5 import { invalidate } from '$app/navigation' 6 - import type { LayoutProps } from './$types' 7 - import Button from '$ui/Button.svelte' 6 + import Button from '$ui/Button/Button.svelte' 7 + import DropdownMenuContent from '$ui/DropdownMenu/DropdownMenuContent.svelte' 8 8 import DropdownMenuItem from '$ui/DropdownMenu/DropdownMenuItem.svelte' 9 + import DropdownMenuTrigger from '$ui/DropdownMenu/DropdownMenuTrigger.svelte' 9 10 import Header from '$ui/Site/Header.svelte' 10 11 import Profile from '$ui/Profile.svelte' 11 12 import ProfilePicture from '$ui/ProfilePicture.svelte' 12 13 import Separator from '$ui/Separator.svelte' 14 + import type { LayoutProps } from './$types' 13 15 import './../app.css' 14 16 import './../fonts.css' 15 17 16 18 type Props = WithChildren<LayoutProps> 17 - 18 19 let { data, children }: Props = $props() 19 20 let { session, supabase, user } = $derived(data) 20 21 ··· 37 38 <meta name="theme-color" content="#151512" media="(prefers-color-scheme: dark)"> 38 39 </svelte:head> 39 40 41 + {#snippet loggedInDropdown(pfpUrl: string, username: string, userId: string)} 42 + <DropdownMenu.Root> 43 + <DropdownMenuTrigger> 44 + <ProfilePicture src={pfpUrl} /> 45 + </DropdownMenuTrigger> 46 + <DropdownMenu.Portal> 47 + <DropdownMenuContent> 48 + <Profile pfpUrl={pfpUrl} username={username} /> 49 + <Separator class="my-2" /> 50 + <DropdownMenuItem href={'/'}> 51 + <HouseIcon class="size-5" strokeWidth={1.5} /> 52 + Home 53 + </DropdownMenuItem> 54 + <DropdownMenuItem href={`/u/${userId}`}> 55 + <UserIcon class="size-5" strokeWidth={1.5} /> 56 + Profile 57 + </DropdownMenuItem> 58 + <DropdownMenuItem href={`/characters/new`}> 59 + <UserPlusIcon class="size-5" strokeWidth={1.5} /> 60 + New Character 61 + </DropdownMenuItem> 62 + <DropdownMenuItem> 63 + <SettingsIcon class="size-5" strokeWidth={1.5} /> 64 + Settings 65 + </DropdownMenuItem> 66 + <Separator class="my-2" /> 67 + <DropdownMenuItem onclick={() => signOut()}> 68 + <LogOutIcon class="size-5" strokeWidth={1.5} /> 69 + Sign Out 70 + </DropdownMenuItem> 71 + </DropdownMenuContent> 72 + </DropdownMenu.Portal> 73 + </DropdownMenu.Root> 74 + {/snippet} 75 + 40 76 <div class={["flex flex-col gap-4 h-screen"]}> 41 77 <Header> 42 78 {#if user} 43 79 {@const pfpUrl = user.user_metadata.avatar_url} 44 80 {@const username = user.user_metadata.full_name} 45 - 46 - <DropdownMenu.Root> 47 - <DropdownMenu.Trigger class={[ 48 - "flex flex-row justify-end w-[230px]", 49 - "outline-hidden focus-visible:outline-hidden", 50 - ]}> 51 - <ProfilePicture src={pfpUrl} /> 52 - </DropdownMenu.Trigger> 53 - <DropdownMenu.Portal> 54 - <DropdownMenu.Content class={[ 55 - "flex flex-col p-2.5 mt-4 w-[230px]", 56 - "outline-hidden focus-visible:outline-hidden", 57 - "bg-white dark:bg-hsr-dark", 58 - "border-2 border-zinc-200 dark:border-zinc-800", 59 - "rounded-lg shadow-xl", 60 - "font-din text-md", 61 - ]}> 62 - <Profile pfpUrl={pfpUrl} username={username} /> 63 - <Separator class="my-2" /> 64 - <DropdownMenuItem href={'/'}> 65 - <HouseIcon class="size-5" strokeWidth={1.5} /> 66 - Home 67 - </DropdownMenuItem> 68 - <DropdownMenuItem href={`/u/${user.id}`}> 69 - <UserIcon class="size-5" strokeWidth={1.5} /> 70 - Profile 71 - </DropdownMenuItem> 72 - <DropdownMenuItem href={`/characters/new`}> 73 - <UserPlusIcon class="size-5" strokeWidth={1.5} /> 74 - New Character 75 - </DropdownMenuItem> 76 - <DropdownMenuItem> 77 - <SettingsIcon class="size-5" strokeWidth={1.5} /> 78 - Settings 79 - </DropdownMenuItem> 80 - <Separator class="my-2" /> 81 - <DropdownMenuItem onclick={() => signOut()}> 82 - <LogOutIcon class="size-5" strokeWidth={1.5} /> 83 - Sign Out 84 - </DropdownMenuItem> 85 - </DropdownMenu.Content> 86 - </DropdownMenu.Portal> 87 - </DropdownMenu.Root> 81 + {@render loggedInDropdown(pfpUrl, username, user.id)} 88 82 {:else} 89 83 <Button onclick={() => signIn()}> 90 84 <LogInIcon class="size-5" />
+2 -8
app/src/routes/+layout.ts
··· 15 15 cookies: { getAll: () => data.cookies } 16 16 }) 17 17 18 - const { 19 - data: { session }, 20 - } = await supabase.auth.getSession() 21 - 22 - const { 23 - data: { user }, 24 - } = await supabase.auth.getUser() 25 - 18 + const { data: { session } } = await supabase.auth.getSession() 19 + const { data: { user } } = await supabase.auth.getUser() 26 20 return { session, supabase, user } 27 21 }
+1 -1
app/src/routes/+page.svelte
··· 38 38 39 39 <PageLayout direction="col" items="stretch" class="gap-18"> 40 40 <div class="flex flex-col gap-2 mt-12"> 41 - <span class="font-din italic text-zinc-300 dark:text-zinc-700"> 41 + <span class="font-sans italic text-zinc-300 dark:text-zinc-700"> 42 42 A fanmade, homebrew campaign based on Honkai: Star Rail 43 43 </span> 44 44 <p class="w-[60ch] text-balance text-xl font-serif font-light">
+38 -23
app/src/routes/combat/+page.svelte
··· 1 - <script lang="ts"> 2 - import Checkbox from '$ui/Checkbox.svelte' 1 + <script lang="ts">import { Checkbox as BitsCheckbox } from 'bits-ui' 2 + import AbilityCard from '$patterns/AbilityCard.svelte' 3 + import Checkbox from '$ui/ChoiceField/Checkbox.svelte' 4 + import CheckboxGroupLabel from '$ui/ChoiceField/CheckboxGroupLabel.svelte' 3 5 import PageLayout from '$ui/Site/PageLayout.svelte' 4 - import AbilityCard from '$patterns/AbilityCard/AbilityCard.svelte' 5 6 import type { PageProps } from './$types' 6 7 7 8 let { data }: PageProps = $props() 8 9 const abilities = data.abilities 10 + 11 + let offensive = $state<string[]>([]) 12 + let defensive = $state<string[]>([]) 13 + let mechanics = $derived([...offensive, ...defensive]) 14 + 15 + const filteredAbilities = $derived.by(() => { 16 + return mechanics.length === 0 17 + ? abilities 18 + : abilities.filter((ability) => mechanics.includes(ability.mechanic)) 19 + }) 9 20 </script> 10 21 11 22 <svelte:head> ··· 21 32 "col-span-2 h-full", 22 33 "flex flex-col gap-8", 23 34 ]}> 24 - <section class="flex flex-col gap-2"> 25 - <span class="text-hsr-gold uppercase font-din font-medium">Offensive</span> 26 - <span class="flex flex-col gap-2"> 27 - <Checkbox name="offensive" label="Single Target" /> 28 - <Checkbox name="offensive" label="AoE" /> 29 - <Checkbox name="offensive" label="Bounce" /> 30 - <Checkbox name="offensive" label="Blast" /> 31 - <Checkbox name="offensive" label="Impair" /> 32 - </span> 33 - </section> 35 + <BitsCheckbox.Group 36 + class="flex flex-col gap-2" 37 + name="offensive" 38 + bind:value={offensive} 39 + > 40 + <CheckboxGroupLabel>Offensive</CheckboxGroupLabel> 41 + <Checkbox label="Single Target" value="single-target" /> 42 + <Checkbox label="AoE" value="aoe" /> 43 + <Checkbox label="Bounce" value="bounce" /> 44 + <Checkbox label="Blast" value="blast" /> 45 + <Checkbox label="Impair" value="impair" /> 46 + </BitsCheckbox.Group> 34 47 35 - <section class="flex flex-col gap-2"> 36 - <span class="text-hsr-gold uppercase font-din font-medium">Defensive</span> 37 - <span class="flex flex-col gap-2"> 38 - <Checkbox name="defensive" label="Support" /> 39 - <Checkbox name="defensive" label="Restore" /> 40 - <Checkbox name="defensive" label="Enhance" /> 41 - <Checkbox name="defensive" label="Defense" /> 42 - </span> 43 - </section> 48 + <BitsCheckbox.Group 49 + class="flex flex-col gap-2" 50 + name="defensive" 51 + bind:value={defensive} 52 + > 53 + <CheckboxGroupLabel>Offensive</CheckboxGroupLabel> 54 + <Checkbox label="Support" value="support" /> 55 + <Checkbox label="Restore" value="restore" /> 56 + <Checkbox label="Enhance" value="enhance" /> 57 + <Checkbox label="Defense" value="defense" /> 58 + </BitsCheckbox.Group> 44 59 </aside> 45 60 46 61 <section class={[ 47 62 "col-span-10 overflow-y-auto", 48 63 "grid grid-cols-2 gap-7", 49 64 ]}> 50 - {#each abilities as ability (ability.name)} 65 + {#each filteredAbilities as ability(ability.name)} 51 66 <AbilityCard {...ability} /> 52 67 {/each} 53 68 </section>
+1 -2
app/src/routes/species/[page=species]/+page.svelte
··· 22 22 'p-2 mb-2', 23 23 'rounded-lg', 24 24 'bg-white dark:bg-hsr-dark', 25 - 'border', 26 - 'border-zinc-200 dark:border-zinc-800', 25 + 'border border-zinc-200 dark:border-zinc-800', 27 26 ], 28 27 }, 29 28 })
+91
app/src/routes/species/new/+page.svelte
··· 1 + <script lang="ts"> 2 + import FormButton from '$ui/Button/FormButton.svelte' 3 + import CheckboxCard from '$ui/ChoiceField/CheckboxCard.svelte' 4 + import Heading from '$ui/Heading/Heading.svelte' 5 + import HeadingGroup from '$ui/Heading/HeadingGroup.svelte' 6 + import SubHeading from '$ui/Heading/SubHeading.svelte' 7 + import PageLayout from '$ui/Site/PageLayout.svelte' 8 + import MultiTextField from '$ui/TextField/MultiTextField.svelte' 9 + import TextField from '$ui/TextField/TextField.svelte' 10 + 11 + type AbilityRecord = { 12 + name: string, 13 + abbr: string, 14 + desc: string, 15 + } 16 + const abilityList: AbilityRecord[] = [ 17 + { 18 + name: 'Strength', 19 + abbr: 'STR', 20 + desc: 'A quality of physical prowess and vigor.', 21 + }, 22 + { 23 + name: 'Dexterity', 24 + abbr: 'DEX', 25 + desc: 'A quality of agility and swiftness.', 26 + }, 27 + { 28 + name: 'Constitution', 29 + abbr: 'CON', 30 + desc: 'A quality of resistance and endurance.', 31 + }, 32 + { 33 + name: 'Intelligence', 34 + abbr: 'INT', 35 + desc: 'A quality of reasoning and comprehension.', 36 + }, 37 + { 38 + name: 'Wisdom', 39 + abbr: 'WIS', 40 + desc: 'A quality of perceptive attunement.', 41 + }, 42 + { 43 + name: 'Charisma', 44 + abbr: 'CHA', 45 + desc: 'A quality of social appeal and glamour.', 46 + }, 47 + ] 48 + </script> 49 + 50 + <PageLayout display="flex" direction="col" class="gap-8 mx-auto" items="stretch"> 51 + <HeadingGroup> 52 + <Heading level={1}>Register a new species</Heading> 53 + <SubHeading isScript>Register a new species</SubHeading> 54 + </HeadingGroup> 55 + <form class="flex flex-col items-start gap-10 max-w-100ch"> 56 + <TextField 57 + name="name" 58 + label="Species name" 59 + placeholder="Enter species name..." 60 + maxlength={20} /> 61 + <MultiTextField 62 + name="description" 63 + label="Species description" 64 + placeholder="Describe the species..." 65 + help="What do they look like? What are they capable of?" 66 + rows={7} /> 67 + <div class="flex flex-col gap-6"> 68 + <div class="leading-relaxed"> 69 + <span class="font-medium">Proficient abilities</span> 70 + <p class="text-sm text-zinc-500">Select up to 2 abilities.</p> 71 + </div> 72 + <div class="grid grid-cols-3 gap-4"> 73 + {#each abilityList as ability} 74 + <CheckboxCard 75 + name={'ability'} 76 + title={ability.name} 77 + desc={ability.desc} /> 78 + {/each} 79 + </div> 80 + </div> 81 + <div class="flex flex-col gap-6"> 82 + <span class="font-medium">Movement</span> 83 + <div class="grid grid-cols-3 gap-4"> 84 + 85 + </div> 86 + </div> 87 + <div class="flex flex-row gap-4"> 88 + <FormButton type="submit" value="Register species" /> 89 + </div> 90 + </form> 91 + </PageLayout>
+1 -2
app/src/stories/AbilityCard.stories.svelte
··· 1 1 <script module> 2 2 import { defineMeta } from '@storybook/addon-svelte-csf' 3 - import AbilityCard from '$patterns/AbilityCard/AbilityCard.svelte' 3 + import AbilityCard from '$patterns/AbilityCard.svelte' 4 4 import { ElementInputType } from './utils' 5 5 6 - // More on how to set up stories at: https://storybook.js.org/docs/writing-stories 7 6 const { Story } = defineMeta({ 8 7 title: 'Patterns/AbilityCard', 9 8 component: AbilityCard,
-1
app/src/stories/Chip.stories.svelte
··· 3 3 import Chip from '$ui/Chip/Chip.svelte' 4 4 import { ColorInputType, SizeInputType, StyleInputType } from './utils' 5 5 6 - // More on how to set up stories at: https://storybook.js.org/docs/writing-stories 7 6 const { Story } = defineMeta({ 8 7 title: 'Components/Chip', 9 8 component: Chip,
-1
app/src/stories/ElementChip.stories.svelte
··· 3 3 import ElementChip from '$patterns/Element/ElementChip.svelte' 4 4 import { ElementInputType, SizeInputType, StyleInputType } from './utils' 5 5 6 - // More on how to set up stories at: https://storybook.js.org/docs/writing-stories 7 6 const { Story } = defineMeta({ 8 7 title: 'Patterns/ElementChip', 9 8 component: ElementChip,
-1
app/src/stories/MechanicChip.stories.svelte
··· 3 3 import MechanicChip from '$patterns/Mechanic/MechanicChip.svelte' 4 4 import { MechanicInputType, SizeInputType, StyleInputType } from './utils' 5 5 6 - // More on how to set up stories at: https://storybook.js.org/docs/writing-stories 7 6 const { Story } = defineMeta({ 8 7 title: 'Patterns/MechanicChip', 9 8 component: MechanicChip,
+3 -2
app/src/stories/utils.ts
··· 1 + import { DefenseMechanicArray, ElementArray, OffenseMechanicArray } from '@starlight/types/hsr' 1 2 import type { InputType } from 'storybook/internal/csf' 2 3 3 4 export const ElementInputType: InputType = { 4 5 control: { type: 'select' }, 5 - options: ['physical', 'fire', 'ice', 'quantum', 'lightning', 'wind', 'imaginary'], 6 + options: ElementArray, 6 7 } 7 8 8 9 export const MechanicInputType: InputType = { 9 10 control: { type: 'select' }, 10 - options: ['single-target', 'aoe', 'bounce', 'blast', 'impair'], 11 + options: [...OffenseMechanicArray, ...DefenseMechanicArray], 11 12 } 12 13 13 14 export const SizeInputType: InputType = {
-2
app/vitest-setup-client.ts
··· 1 - /// <reference types="@vitest/browser/matchers" /> 2 - /// <reference types="@vitest/browser/providers/playwright" />
+23 -1
package-lock.json
··· 30 30 "dependencies": { 31 31 "@cloudflare/workers-types": "^4.20251107.0", 32 32 "@lucide/svelte": "^0.553.0", 33 + "@starlight/color": "file:../packages/color", 33 34 "@starlight/icons": "file:../packages/icons", 34 35 "@starlight/tokenizer": "file:../packages/tokenizer", 35 36 "@starlight/types": "file:../packages/types", ··· 2293 2294 "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", 2294 2295 "devOptional": true, 2295 2296 "license": "MIT" 2297 + }, 2298 + "node_modules/@starlight/color": { 2299 + "resolved": "packages/color", 2300 + "link": true 2296 2301 }, 2297 2302 "node_modules/@starlight/icons": { 2298 2303 "resolved": "packages/icons", ··· 8191 8196 "url": "https://github.com/sponsors/colinhacks" 8192 8197 } 8193 8198 }, 8199 + "packages/color": { 8200 + "name": "@starlight/color", 8201 + "version": "1.0.0", 8202 + "license": "MIT" 8203 + }, 8194 8204 "packages/icons": { 8195 8205 "name": "@starlight/icons", 8196 8206 "version": "0.0.1", ··· 8219 8229 "packages/types": { 8220 8230 "name": "@starlight/types", 8221 8231 "version": "1.0.0", 8222 - "license": "MIT" 8232 + "license": "MIT", 8233 + "dependencies": { 8234 + "zod": "^4.1.12" 8235 + } 8236 + }, 8237 + "packages/types/node_modules/zod": { 8238 + "version": "4.1.12", 8239 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", 8240 + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", 8241 + "license": "MIT", 8242 + "funding": { 8243 + "url": "https://github.com/sponsors/colinhacks" 8244 + } 8223 8245 } 8224 8246 } 8225 8247 }
+25
packages/color/package.json
··· 1 + { 2 + "private": true, 3 + "name": "@starlight/color", 4 + "author": "Samantha Nguyen", 5 + "version": "1.0.0", 6 + "license": "MIT", 7 + "type": "module", 8 + "types": "./dist/index.d.ts", 9 + "module": "./dist/index.js", 10 + "exports": { 11 + ".": { 12 + "types": "./dist/index.d.ts", 13 + "import": "./dist/index.js" 14 + } 15 + }, 16 + "files": [ 17 + "./dist" 18 + ], 19 + "scripts": { 20 + "build": "tsdown", 21 + "dev": "tsdown --watch", 22 + "test": "vitest --typecheck.enabled --coverage", 23 + "test-ui": "vitest --ui --typecheck.enabled --coverage" 24 + } 25 + }
+9
packages/color/tsdown.config.ts
··· 1 + import { defineConfig } from 'tsdown' 2 + 3 + export default defineConfig({ 4 + entry: 'src/index.ts', 5 + platform: 'browser', 6 + minify: true, 7 + dts: true, 8 + publint: true, 9 + })
+3
packages/types/package.json
··· 23 23 "dev": "tsdown --watch", 24 24 "test": "vitest --typecheck.enabled --coverage", 25 25 "test-ui": "vitest --ui --typecheck.enabled --coverage" 26 + }, 27 + "dependencies": { 28 + "zod": "^4.1.12" 26 29 } 27 30 }
+21 -3
packages/types/src/dnd.ts
··· 1 1 import type { IntClosedRange } from 'type-fest' 2 + import z from 'zod' 2 3 3 4 export type CharacterLevel = IntClosedRange<1, 20> 4 5 export type D4 = IntClosedRange<1, 4> ··· 23 24 | D12Notation 24 25 | D20Notation 25 26 26 - export const AbilityModArray = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA'] as const 27 - export type AbilityMod = typeof AbilityModArray[number] 28 - 29 27 export type HitDc = `${AbilityMod} ${number}` 30 28 29 + export const AbilityModArray = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA'] as const 31 30 export const SpellComponentArray = ['V', 'S', 'M'] as const 31 + export const SpellSchoolArray = [ 32 + 'abjuration', 33 + 'conjuration', 34 + 'divination', 35 + 'enchantment', 36 + 'evocation', 37 + 'illusion', 38 + 'necromany', 39 + 'transmutation', 40 + ] as const 41 + 42 + /* runtime types */ 43 + export const AbilityModEnum = z.enum(AbilityModArray) 44 + export const SpellComponentEnum = z.enum(SpellComponentArray) 45 + export const SpellSchoolEnum = z.enum(SpellSchoolArray) 46 + 47 + /* compile-time types */ 48 + export type AbilityMod = typeof AbilityModArray[number] 32 49 export type SpellComponent = typeof SpellComponentArray[number] 50 + export type SpellSchool = typeof SpellSchoolArray[number]
+9 -2
packages/types/src/hsr.ts
··· 1 - /* array backings */ 1 + import { z } from 'zod' 2 + 2 3 export const ElementArray = [ 3 4 'fire', 4 5 'ice', ··· 33 34 'defense', 34 35 ] as const 35 36 36 - /* types */ 37 + /* runtime types */ 38 + export const ElementEnum = z.enum(ElementArray) 39 + export const SpeciesEnum = z.enum(SpeciesArray) 40 + export const OffenseMechanicEnum = z.enum(OffenseMechanicArray) 41 + export const DefenseMechanicEnum = z.enum(DefenseMechanicArray) 42 + 43 + /* compile-time types */ 37 44 export type Element = typeof ElementArray[number] 38 45 export type Species = typeof SpeciesArray[number] 39 46 export type OffenseMechanic = typeof OffenseMechanicArray[number]
+1
scripts/build.sh
··· 1 1 #!/bin/sh 2 2 3 3 npm i 4 + npm run build --workspace=packages/color 4 5 npm run build --workspace=packages/icons 5 6 npm run build --workspace=packages/types 6 7 npm run build --workspace=packages/tokenizer