diff --git a/.changeset/clever-avocados-hope.md b/.changeset/clever-avocados-hope.md new file mode 100644 index 000000000..26f562bfe --- /dev/null +++ b/.changeset/clever-avocados-hope.md @@ -0,0 +1,5 @@ +--- +"svelte-ux": minor +--- + +Add ability to set a default values for labelPlacement and variant props diff --git a/packages/create-svelte-ux/templates/starter/src/routes/+layout.svelte b/packages/create-svelte-ux/templates/starter/src/routes/+layout.svelte index cdf1cd426..a90556081 100644 --- a/packages/create-svelte-ux/templates/starter/src/routes/+layout.svelte +++ b/packages/create-svelte-ux/templates/starter/src/routes/+layout.svelte @@ -5,13 +5,19 @@ import '../app.postcss'; settings({ - classes: { - AppBar: 'bg-primary text-white shadow-md', + components: { + AppBar: { + classes: 'bg-primary text-white shadow-md' + }, AppLayout: { - nav: 'bg-neutral-800' + classes: { + nav: 'bg-neutral-800' + } }, NavItem: { - root: 'text-sm text-gray-400 pl-6 py-2 hover:text-white hover:bg-gray-300/10 [&:where(.is-active)]:text-sky-400 [&:where(.is-active)]:bg-gray-500/10' + classes: { + root: 'text-sm text-gray-400 pl-6 py-2 hover:text-white hover:bg-gray-300/10 [&:where(.is-active)]:text-sky-400 [&:where(.is-active)]:bg-gray-500/10' + } } } }); diff --git a/packages/svelte-ux/src/lib/components/Button.svelte b/packages/svelte-ux/src/lib/components/Button.svelte index 8a9520440..da7948a62 100644 --- a/packages/svelte-ux/src/lib/components/Button.svelte +++ b/packages/svelte-ux/src/lib/components/Button.svelte @@ -6,10 +6,13 @@ import { cls } from '../utils/styles'; import { multi } from '../actions/multi'; import type { Actions } from '../actions/multi'; - import type { ThemeColors } from '$lib/types'; - import { getComponentClasses } from './theme'; + import type { ButtonColor, ButtonSize } from '$lib/types'; import { getButtonGroup } from './ButtonGroup.svelte'; import { asIconData, type IconInput } from '$lib/utils/icons'; + import type { ButtonRounded, ButtonVariant } from '$lib/types'; + import { getComponentSettings } from './settings'; + + const { classes: settingsClasses, defaults } = getComponentSettings('Button'); export let type: 'button' | 'submit' | 'reset' = 'button'; export let href: string | undefined = undefined; @@ -21,18 +24,10 @@ export let loading: boolean = false; export let disabled: boolean = false; - export let rounded: boolean | 'full' | undefined = undefined; // default in reactive groupContext below - export let variant: - | 'default' - | 'outline' - | 'fill' - | 'fill-outline' - | 'fill-light' - | 'text' - | 'none' - | undefined = undefined; // default in reactive groupContext below - export let size: 'sm' | 'md' | 'lg' | undefined = undefined; // default in reactive groupContext below - export let color: ThemeColors | 'default' | undefined = undefined; // default in reactive groupContext below + export let rounded: ButtonRounded | undefined = undefined; // default in reactive groupContext below + export let variant: ButtonVariant | undefined = undefined; // default in reactive groupContext below + export let size: ButtonSize | undefined = undefined; // default in reactive groupContext below + export let color: ButtonColor | undefined = undefined; // default in reactive groupContext below /** @type {{root?: string, icon?: string, loading?: string}} */ export let classes: { @@ -40,14 +35,13 @@ icon?: string; loading?: string; } = {}; - const settingsClasses = getComponentClasses('Button'); // Override default from `ButtonGroup` if set const groupContext = getButtonGroup(); - $: variant = variant ?? groupContext?.variant ?? 'default'; - $: size = size ?? groupContext?.size ?? 'md'; - $: color = color ?? groupContext?.color ?? 'default'; - $: rounded = rounded ?? groupContext?.rounded ?? (iconOnly ? 'full' : true); + $: variant = variant ?? groupContext?.variant ?? defaults.variant ?? 'default'; + $: size = size ?? groupContext?.size ?? defaults.size ?? 'md'; + $: color = color ?? groupContext?.color ?? defaults.color ?? 'default'; + $: rounded = rounded ?? groupContext?.rounded ?? defaults.rounded ?? (iconOnly ? 'full' : true); $: _class = cls( 'Button', diff --git a/packages/svelte-ux/src/lib/components/ButtonGroup.svelte b/packages/svelte-ux/src/lib/components/ButtonGroup.svelte index 26cf68619..d44b6d029 100644 --- a/packages/svelte-ux/src/lib/components/ButtonGroup.svelte +++ b/packages/svelte-ux/src/lib/components/ButtonGroup.svelte @@ -1,23 +1,14 @@ {/if} @@ -30,7 +35,7 @@ {/if} {#if buttonPlacement === 'after'} - {/if} diff --git a/packages/svelte-ux/src/lib/components/index.ts b/packages/svelte-ux/src/lib/components/index.ts index 8d3c7ce2c..48299dc00 100644 --- a/packages/svelte-ux/src/lib/components/index.ts +++ b/packages/svelte-ux/src/lib/components/index.ts @@ -96,5 +96,5 @@ export { default as TreeList } from './TreeList.svelte'; export { default as TweenedValue } from './TweenedValue.svelte'; export { default as ViewportCenter } from './ViewportCenter.svelte'; export { default as YearList } from './YearList.svelte'; -export { settings, getSettings } from './settings'; -export { getClasses, getComponentClasses } from './theme'; +export { settings, getSettings, getComponentSettings } from './settings'; +export { getComponentClasses } from './theme'; diff --git a/packages/svelte-ux/src/lib/components/settings.ts b/packages/svelte-ux/src/lib/components/settings.ts index bf912f321..084036a15 100644 --- a/packages/svelte-ux/src/lib/components/settings.ts +++ b/packages/svelte-ux/src/lib/components/settings.ts @@ -1,6 +1,13 @@ import { getContext, setContext } from 'svelte'; -import type { ComponentClasses } from './theme'; +import { + type ComponentName, + type ComponentSettings, + type ResolvedComponentSettings, + resolveComponentClasses, + type ResolvedDefaultProps, +} from './theme'; import { createThemeStore, type ThemeStore } from '$lib/stores/themeStore'; +import type { LabelPlacement } from '$lib/types'; import { getAllKnownLocales, localeStore, @@ -9,7 +16,11 @@ import { type LocaleSettingsInput, } from '$lib/utils/locale'; import { buildFormatters, type FormatFunctions } from '$lib/utils/format'; -import { writable, type Readable, type Writable, derived } from 'svelte/store'; +import { type Readable, derived } from 'svelte/store'; + +export interface DefaultProps { + labelPlacement: LabelPlacement; +} export type SettingsInput = { /** Force a specific locale setting. */ @@ -20,7 +31,7 @@ export type SettingsInput = { /** Format information for additional locales that are not built-in to svelte-ux. */ localeFormats?: Record; - classes?: ComponentClasses; + components?: ComponentSettings; /** A list of the available themes */ themes?: { light?: string[]; @@ -44,6 +55,8 @@ export interface Settings extends Omit /** Formatting functions and information */ format: Readable; currentTheme: ThemeStore; + + componentSettingsCache: Partial>>; } const settingsKey = Symbol(); @@ -100,6 +113,7 @@ export function settings(settings: SettingsInput): Settings { dark: darkThemes, }, currentTheme, + componentSettingsCache: {}, ...localeStores, }); } @@ -111,6 +125,37 @@ export function getSettings(): Settings { } catch (error) { return { currentTheme: createThemeStore({ light: ['light'], dark: ['dark'] }), + componentSettingsCache: {}, }; } } + +export function resolveComponentSettings( + settings: Settings, + name: NAME +): ResolvedComponentSettings { + const { classes: themeClasses, ...defaultProps } = settings?.components?.[name] ?? {}; + const classes = resolveComponentClasses(themeClasses); + + const output: ResolvedComponentSettings = { + defaults: (defaultProps ?? {}) as ResolvedDefaultProps, + classes, + }; + + return output; +} + +export function getComponentSettings( + name: NAME +): ResolvedComponentSettings { + const settings = getSettings(); + + const existing = settings.componentSettingsCache[name]; + if (existing) { + return existing as ResolvedComponentSettings; + } + + const output = resolveComponentSettings(settings, name)!; + settings.componentSettingsCache[name] = output; + return output; +} diff --git a/packages/svelte-ux/src/lib/components/theme.ts b/packages/svelte-ux/src/lib/components/theme.ts index a05737cc7..3c1dfa903 100644 --- a/packages/svelte-ux/src/lib/components/theme.ts +++ b/packages/svelte-ux/src/lib/components/theme.ts @@ -1,8 +1,15 @@ import type { ComponentProps, SvelteComponent } from 'svelte'; import type * as Components from './'; import { getSettings } from './settings'; +import type { + ButtonColor, + ButtonRounded, + ButtonSize, + ButtonVariant, + LabelPlacement, +} from '$lib/types'; -type ComponentName = keyof typeof Components; +export type ComponentName = keyof typeof Components; type ClassesProp = T extends { prototype: infer PR extends SvelteComponent } ? ComponentProps extends { classes?: any } @@ -10,16 +17,105 @@ type ClassesProp = T extends { prototype: infer PR extends SvelteComponent } : never : never; -export type ComponentClasses = { - [key in ComponentName]?: ClassesProp<(typeof Components)[key]> | string; +interface ComponentDefaultProps { + Button?: { + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + rounded?: ButtonRounded; + }; + ButtonGroup?: { + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + rounded?: ButtonRounded; + }; + CopyButton?: { + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + rounded?: ButtonRounded; + }; + DateButton?: { + variant?: ButtonVariant; + }; + DateField?: { + labelPlacement?: LabelPlacement; + }; + DatePickerField?: { + labelPlacement?: LabelPlacement; + }; + DateRangeField?: { + labelPlacement?: LabelPlacement; + }; + Field?: { + labelPlacement?: LabelPlacement; + }; + MenuButton?: { + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + rounded?: ButtonRounded; + }; + MenuField?: { + labelPlacement?: LabelPlacement; + }; + MultiSelectField?: { + labelPlacement?: LabelPlacement; + }; + RangeField?: { + labelPlacement?: LabelPlacement; + }; + SelectField?: { + labelPlacement?: LabelPlacement; + }; + TextField?: { + labelPlacement?: LabelPlacement; + }; + ToggleButton?: { + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + rounded?: ButtonRounded; + }; +} + +export type ResolvedComponentClasses = { + [key in ComponentName]: ResolvedComponentClassesProp; }; -export function getClasses() { - return getSettings().classes ?? {}; +export type ResolvedComponentClassesProp = + ClassesProp<(typeof Components)[NAME]> extends never + ? {} + : NonNullable>; + +export type ResolvedDefaultProps = + NAME extends keyof ComponentDefaultProps ? NonNullable : {}; + +export interface ResolvedComponentSettings { + defaults: ResolvedDefaultProps; + classes: ResolvedComponentClassesProp; } -export function getComponentClasses(name: ComponentName) { - const theme = getClasses()[name] ?? {}; +export type ComponentSettings = { + [key in ComponentName]?: { + classes?: ClassesProp<(typeof Components)[key]> | string; + } & (key extends keyof ComponentDefaultProps ? ComponentDefaultProps[key] : {}); +}; + +export function getComponents(): ComponentSettings { + return getSettings().components ?? {}; +} + +export function resolveComponentClasses( + theme: ClassesProp<(typeof Components)[NAME]> +): ResolvedComponentClassesProp { + return typeof theme === 'string' ? { root: theme } : theme ?? {}; +} - return typeof theme === 'string' ? { root: theme } : theme; +export function getComponentClasses( + name: NAME +): ResolvedComponentClasses[NAME] { + const settings = getSettings(); + return resolveComponentClasses(settings?.components?.[name]?.classes); } diff --git a/packages/svelte-ux/src/lib/types/index.ts b/packages/svelte-ux/src/lib/types/index.ts index a705e8b41..b11a8b75a 100644 --- a/packages/svelte-ux/src/lib/types/index.ts +++ b/packages/svelte-ux/src/lib/types/index.ts @@ -1,3 +1,28 @@ +import type { ThemeColors } from './typeHelpers'; + export * from './table'; export * from './typeHelpers'; export * from './typeGuards'; + +export type MenuOption = { + label: string; + value: any; + icon?: string; + group?: string; +}; + +export type LabelPlacement = 'inset' | 'float' | 'top' | 'left'; +export const DEFAULT_LABEL_PLACEMENT: LabelPlacement = 'inset'; + +export type ButtonVariant = + | 'default' + | 'outline' + | 'fill' + | 'fill-outline' + | 'fill-light' + | 'text' + | 'none'; + +export type ButtonColor = ThemeColors | 'default'; +export type ButtonSize = 'sm' | 'md' | 'lg'; +export type ButtonRounded = boolean | 'full'; diff --git a/packages/svelte-ux/src/lib/types/options.ts b/packages/svelte-ux/src/lib/types/options.ts deleted file mode 100644 index 04a42dfce..000000000 --- a/packages/svelte-ux/src/lib/types/options.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type MenuOption = { - label: string; - value: any; - icon?: string; - group?: string; -}; diff --git a/packages/svelte-ux/src/routes/+layout.svelte b/packages/svelte-ux/src/routes/+layout.svelte index 4eea10a4e..d1cd47b10 100644 --- a/packages/svelte-ux/src/routes/+layout.svelte +++ b/packages/svelte-ux/src/routes/+layout.svelte @@ -111,17 +111,23 @@ }), }, - classes: { + components: { AppLayout: { - aside: 'border-r', - nav: 'bg-surface-300 py-2', + classes: { + aside: 'border-r', + nav: 'bg-surface-300 py-2', + }, + }, + AppBar: { + classes: + 'bg-primary text-primary-content shadow-md [text-shadow:1px_1px_2px_theme(colors.primary-700)]', }, - AppBar: - 'bg-primary text-primary-content shadow-md [text-shadow:1px_1px_2px_theme(colors.primary-700)]', NavItem: { - root: 'text-sm text-surface-content/70 pl-6 py-2 hover:bg-surface-100/70 relative', - active: - 'text-primary bg-surface-100 font-medium before:absolute before:bg-primary before:rounded-full before:w-1 before:h-2/3 before:left-[6px] shadow z-10', + classes: { + root: 'text-sm text-surface-content/70 pl-6 py-2 hover:bg-surface-100/70 relative', + active: + 'text-primary bg-surface-100 font-medium before:absolute before:bg-primary before:rounded-full before:w-1 before:h-2/3 before:left-[6px] shadow z-10', + }, }, }, themes: data.themes, diff --git a/packages/svelte-ux/src/routes/customization/+page.md b/packages/svelte-ux/src/routes/customization/+page.md index efcf3299d..2917d70c0 100644 --- a/packages/svelte-ux/src/routes/customization/+page.md +++ b/packages/svelte-ux/src/routes/customization/+page.md @@ -232,26 +232,40 @@ Two components, `ThemeSelect` and `ThemeSwitch` are available to easily change t ## Settings -At the root of your app, you can call `settings({ ... })` to set component classes and define formats. Usually this is done in `+layout.svelte`. +At the root of your app, you can call `settings({ ... })` to set component classes, define some default component props such as `variant` and `labelPlacement`, and define locales / formats. Usually this is done in `+layout.svelte`. -For each `ComponentName: ...` you can pass `class` (when value is a `string`) or `classes` (when value is an `object`) props to each component via context to allow convenient global styling, including access to internal elements (when using `classes`) +For each `ComponentName: ...` you can add convenient global styling by passing `classes` as a `string` to alter the root class string, or an `object` for finer control over internal elements. + +Components based on Button and Field also let you customize the default `variant` and `labelPlacement` properties, respectively. ```svelte ``` -`settings()` is also used to define `localeFormats`, which are used by the `format()` util as well as components (such as `DateField`, `DatePickerField`, and `DateRangeField`) +`settings()` is also used to define `localeFormats`, which are used by the `format()` util as well as date components (such as `DateField`, `DatePickerField`, and `DateRangeField`) ```js settings({ @@ -327,15 +341,13 @@ Internally, each component uses the `cls()` util which leverages [tailwind-merge --- ---- - ## Class precedence -Classes are applied in the following order, and [tailwind-merge](https://github.com/dcastil/tailwind-merge) handles overrides (last wins) +Classes are applied in the following order, and [tailwind-merge](https://github.com/dcastil/tailwind-merge) handles conflict resolution (last wins). - Base component classes - Variant specific classes -- Setting `classes` (context) +- settings() `classes` (context) - `classes` prop - `class` prop @@ -343,7 +355,7 @@ Classes are applied in the following order, and [tailwind-merge](https://github. ## Global classes -All components with top-level elements add a `{ComponentName}` class, to allow easy overriding using global CSS rules, if desired. +All components with top-level elements add a `{ComponentName}` class, to allow global CSS rules, if desired. ```css :global(.Button) { @@ -371,11 +383,16 @@ If a default variant is applied (for example Button uses `text`), you can use th