···11+# 🦊 fox ui
22+33+> **This is a public alpha release. Expect bugs and breaking changes.**
44+55+svelte v5 + tailwind v4
66+77+[See all components here](https://flo-bit.dev/ui-kit)
88+99+Read more about [the philosophy/aim of this project here](https://flo-bit.dev/ui-kit/docs/philosophy).
1010+1111+## Quickstart
1212+1313+### 1. Create a new svelte project with tailwind css (including `@tailwindcss/typography` and `@tailwindcss/forms` plugins)
1414+1515+```bash
1616+npx sv create my-project
1717+```
1818+1919+### 2. Install fuchs-ui
2020+2121+```bash
2222+npm install @fuxui/base
2323+```
2424+2525+### 3. set theme variables in your app.css (changing `zinc` and `emerald` to your preferred colors, using find and replace).
2626+2727+```css
2828+@source "../node_modules/@fuxui";
2929+3030+@theme {
3131+ --color-base-50: var(--color-zinc-50);
3232+ --color-base-100: var(--color-zinc-100);
3333+ --color-base-200: var(--color-zinc-200);
3434+ --color-base-300: var(--color-zinc-300);
3535+ --color-base-400: var(--color-zinc-400);
3636+ --color-base-500: var(--color-zinc-500);
3737+ --color-base-600: var(--color-zinc-600);
3838+ --color-base-700: var(--color-zinc-700);
3939+ --color-base-800: var(--color-zinc-800);
4040+ --color-base-900: var(--color-zinc-900);
4141+ --color-base-950: var(--color-zinc-950);
4242+4343+ --color-accent-50: var(--color-emerald-50);
4444+ --color-accent-100: var(--color-emerald-100);
4545+ --color-accent-200: var(--color-emerald-200);
4646+ --color-accent-300: var(--color-emerald-300);
4747+ --color-accent-400: var(--color-emerald-400);
4848+ --color-accent-500: var(--color-emerald-500);
4949+ --color-accent-600: var(--color-emerald-600);
5050+ --color-accent-700: var(--color-emerald-700);
5151+ --color-accent-800: var(--color-emerald-800);
5252+ --color-accent-900: var(--color-emerald-900);
5353+ --color-accent-950: var(--color-emerald-950);
5454+}
5555+```
5656+5757+### 4. Use the components
5858+5959+```svelte
6060+<script>
6161+ import { Button } from '@fuxui/base';
6262+</script>
6363+6464+<Button onclick={() => alert('clicked')}>Click me</Button>
6565+```
6666+6767+## Development
6868+6969+If you want to contribute to the project, please open an issue first describing the feature you want to add.
7070+7171+Clone the repo, install dependencies and run the dev server
7272+7373+```bash
7474+git clone https://github.com/flo-bit/ui-kit.git
7575+cd ui-kit
7676+pnpm install
7777+pnpm run dev
7878+```
7979+8080+The ui-kit library is located in `packages/` (`packages/base` for the base package), the documentation is in `apps/docs`.
···11+import adapter from '@sveltejs/adapter-static';
22+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
33+44+/** @type {import('@sveltejs/kit').Config} */
55+const config = {
66+ // Consult https://svelte.dev/docs/kit/integrations
77+ // for more information about preprocessors
88+ preprocess: vitePreprocess(),
99+1010+ kit: {
1111+ // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
1212+ // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
1313+ // See https://svelte.dev/docs/kit/adapters for more information about adapters.
1414+ adapter: adapter()
1515+ }
1616+};
1717+1818+export default config;
+19
packages/social/tsconfig.json
···11+{
22+ "extends": "./.svelte-kit/tsconfig.json",
33+ "compilerOptions": {
44+ "allowJs": true,
55+ "checkJs": true,
66+ "esModuleInterop": true,
77+ "forceConsistentCasingInFileNames": true,
88+ "resolveJsonModule": true,
99+ "skipLibCheck": true,
1010+ "sourceMap": true,
1111+ "strict": true,
1212+ "moduleResolution": "bundler"
1313+ }
1414+ // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
1515+ // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
1616+ //
1717+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
1818+ // from the referenced tsconfig.json - TypeScript does not merge them in
1919+}
+7
packages/social/vite.config.ts
···11+import tailwindcss from '@tailwindcss/vite';
22+import { sveltekit } from '@sveltejs/kit/vite';
33+import { defineConfig } from 'vite';
44+55+export default defineConfig({
66+ plugins: [tailwindcss(), sveltekit()]
77+});
apps/docs/static/opengraph.png
This is a binary file and will not be displayed.
+26
packages/social/scripts/cleanup.js
···11+import { readFileSync, writeFileSync } from 'fs';
22+33+/**
44+ * This script utilizes a property called "publishOverrides" in the package.json
55+ * to override specific properties in the package.json when publishing to npm.
66+ */
77+const cleanPackage = () => {
88+ let packageJson = JSON.parse(
99+ readFileSync('./package.json', {
1010+ encoding: 'utf-8'
1111+ })
1212+ );
1313+1414+ packageJson = {
1515+ ...packageJson,
1616+ ...packageJson.publishOverrides
1717+ };
1818+1919+ delete packageJson.publishOverrides;
2020+2121+ writeFileSync('./package.json', JSON.stringify(packageJson, null, 2), {
2222+ encoding: 'utf-8'
2323+ });
2424+};
2525+2626+cleanPackage();
···11-export { Accordion, AccordionItem } from './accordion';
22-export { Alert, AlertDescription, AlertTitle } from './alert';
33-export { Avatar, AvatarGroup } from './avatar';
44-export { Badge } from './badge';
55-66-export { Button } from './button';
77-export { type ButtonProps } from './button/Button.svelte';
88-export { type ButtonSize, type ButtonVariant, buttonVariants } from './button/';
99-1010-export { default as Box } from './box/Box.svelte';
1111-export { default as ImageCard } from './card/ImageCard.svelte';
1212-export { default as ChatBubble } from './chat-bubble/ChatBubble.svelte';
1313-export { Checkbox } from './checkbox';
1414-export { default as Head } from './head/Head.svelte';
1515-export { default as Heading } from './heading/Heading.svelte';
1616-export { default as Image } from './image/Image.svelte';
1717-export { type ImageProps } from './image/Image.svelte';
1818-export { default as ImageContainer } from './image-container/ImageContainer.svelte';
1919-export { Input } from './input/';
2020-export { Label } from './label/';
2121-export { default as Modal } from './modal/Modal.svelte';
2222-export { default as NumberInput } from './number-input/NumberInput.svelte';
2323-export { default as Prose } from './prose/Prose.svelte';
2424-export { default as ScrollArea } from './scroll-area/ScrollArea.svelte';
2525-export { Slider } from './slider';
2626-export { Select } from './select/';
2727-export { default as SliderNumber } from './slider/SliderNumber.svelte';
2828-export { Toaster } from './sonner/';
2929-export { toast } from 'svelte-sonner';
3030-export { Switch } from './switch/';
3131-export { Paragraph } from './paragraph/';
3232-export { Textarea } from './textarea';
3333-export { Tooltip } from './tooltip';
3434-export { default as Popover } from './popover/Popover.svelte';
3535-export { default as Subheading } from './heading/Subheading.svelte';
3636-3737-export { default as AddCopyCodeButtons } from './copy-code-button/AddCopyCodeButtons.svelte';
3838-3939-export { Toggle } from './toggle';
4040-export { ToggleGroup, ToggleGroupItem } from './toggle-group';
4141-4242-export { default as BlueskyLoginModal } from './modal/BlueskyLoginModal.svelte';
4343-export { blueskyLoginModalState } from './modal/BlueskyLoginModal.svelte';
4444-4545-export { default as ThemeToggle } from './theme-toggle/ThemeToggle.svelte';
4646-export { default as Navbar } from './navbar/Navbar.svelte';
4747-export { default as Sidebar } from './sidebar/Sidebar.svelte';
11+export * from './accordion';
22+export * from './alert';
33+export * from './avatar';
44+export * from './badge';
55+export * from './box';
66+export * from './button';
77+export * from './card';
88+export * from './chat-bubble';
99+export * from './checkbox';
1010+export * from './head';
1111+export * from './heading';
1212+export * from './image';
1313+export * from './image-container';
1414+export * from './input';
1515+export * from './label';
1616+export * from './modal';
1717+export * from './navbar';
1818+export * from './number-input';
1919+export * from './paragraph';
2020+export * from './popover';
2121+export * from './prose';
2222+export * from './scroll-area';
2323+export * from './select';
2424+export * from './sidebar';
2525+export * from './slider';
2626+export * from './sonner';
2727+export * from './switch';
2828+export * from './textarea';
2929+export * from './theme-toggle';
3030+export * from './toggle';
3131+export * from './toggle-group';
3232+export * from './tooltip';
+7
packages/social/src/lib/components/index.ts
···11+export * from './bluesky-login';
22+export * from './post';
33+export * from './star-rating';
44+export * from './social-icons';
55+export * from './swiper-cards';
66+export * from './user-profile';
77+export * from './github-corner';
+1
packages/visual/src/lib/components/index.ts
···33export * from './phone';
44export * from './quote';
55export * from './undraw';
66+export * from './image-masonry';
···55const Root = AccordionPrimitive.Root;
6677export {
88- Root,
99- Content,
1010- Item,
1111- Trigger,
1212- //
138 Root as Accordion,
149 Content as AccordionContent,
1510 Item as AccordionItem,
+1-9
packages/base/src/lib/components/alert/index.ts
···33import Title from './alert-title.svelte';
44export { alertVariants, type AlertVariant } from './alert.svelte';
5566-export {
77- Root,
88- Description,
99- Title,
1010- //
1111- Root as Alert,
1212- Description as AlertDescription,
1313- Title as AlertTitle
1414-};
66+export { Root as Alert, Description as AlertDescription, Title as AlertTitle };
···11+export { default as Box } from './Box.svelte';
+2-11
packages/base/src/lib/components/button/index.ts
···11-import Root, {
11+export {
22+ default as Button,
23 type ButtonProps,
34 type ButtonSize,
45 type ButtonVariant,
56 buttonVariants
67} from './Button.svelte';
77-88-export {
99- Root,
1010- type ButtonProps as Props,
1111- Root as Button,
1212- buttonVariants,
1313- type ButtonProps,
1414- type ButtonSize,
1515- type ButtonVariant
1616-};
···11-import Root from './checkbox.svelte';
22-export {
33- Root,
44- //
55- Root as Checkbox
66-};
11+export { default as Checkbox } from './checkbox.svelte';
+1
packages/base/src/lib/components/head/index.ts
···11+export { default as Head } from './Head.svelte';
+1-9
packages/base/src/lib/components/input/index.ts
···55 inputVariants
66} from './Input.svelte';
7788-export {
99- Root,
1010- type InputProps as Props,
1111- Root as Input,
1212- inputVariants,
1313- type InputProps,
1414- type InputSize,
1515- type InputVariant
1616-};
88+export { type InputProps, Root as Input, inputVariants, type InputSize, type InputVariant };
+1-7
packages/base/src/lib/components/label/index.ts
···11-import Root from './Label.svelte';
22-33-export {
44- Root,
55- //
66- Root as Label
77-};
11+export { default as Label } from './Label.svelte';
···11+export { default as NumberInput } from './NumberInput.svelte';
22+export type { NumberInputProps, NumberInputVariant, NumberInputSize } from './NumberInput.svelte';
···11+export { default as BlueskyLoginModal } from './BlueskyLoginModal.svelte';
22+export { blueskyLoginModalState } from './BlueskyLoginModal.svelte';
33+export { default as BlueskyLogin } from './BlueskyLogin.svelte';
···11+// all icons from https://simpleicons.org/
22+33+export { default as SocialIcons } from './All.svelte';
44+export { default as Discord } from './Discord.svelte';
55+export { default as Github } from './Github.svelte';
66+export { default as Twitter } from './Twitter.svelte';
77+export { default as Youtube } from './Youtube.svelte';
88+export { default as Bluesky } from './Bluesky.svelte';
99+export { default as Facebook } from './Facebook.svelte';
···31313232So the plan is to have both options (usually you start by just using the package and if you
3333eventually need to customize a component you add it to your project),
3434-currently the recommended way to use it is installing as a package,
3535-but a cli tool is in the works and can already be tested with the base components by running:
3636-3737-```bash
3838-npx fuchs add [component-name]
3939-```
4040-4141-(Note: this still needs you to install `fuchs` as a package first).
3434+currently the only way to use it is installing as a package,
3535+but a cli tool is in the works.
42364337## Lots of components
4438
···11+// keep a cache of formatter per locale, to avoid re-creating them (GC)
22+const formatters = new Map<string, Intl.RelativeTimeFormat>();
33+44+// get the Intl.RelativeTimeFormat formatter for the given locale
55+export function getFormatter(locale: string) {
66+ if (formatters.has(locale)) {
77+ return formatters.get(locale)!;
88+ }
99+ const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' });
1010+ formatters.set(locale, formatter);
1111+ return formatter;
1212+}
···11+// adapted from https://github.com/CaptainCodeman/svelte-relative-time
22+33+export * from './action';
44+export type { Callback } from './render';
55+export { register, unregister } from './state';
66+export { default } from './RelativeTime.svelte';
···11+export type Callback = (result: {
22+ seconds: number;
33+ count: number;
44+ units: Intl.RelativeTimeFormatUnit;
55+ text: string;
66+}) => void;
77+88+export interface RenderState {
99+ date: Date | number;
1010+ callback: Callback;
1111+ formatter: Intl.RelativeTimeFormat;
1212+}
1313+1414+// Array reprsenting one minute, hour, day, week, month, etc in seconds
1515+const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
1616+1717+// Array equivalent to the above but in the string representation of the units
1818+const formatUnits: Intl.RelativeTimeFormatUnit[] = [
1919+ 'seconds',
2020+ 'minutes',
2121+ 'hours',
2222+ 'days',
2323+ 'weeks',
2424+ 'months',
2525+ 'years'
2626+];
2727+2828+// function to render relative time into
2929+export function render(state: RenderState, now: number) {
3030+ const { date, callback, formatter } = state;
3131+3232+ // Allow dates or times to be passed
3333+ const timeMs = typeof date === 'number' ? date : date.getTime();
3434+3535+ // Get the amount of seconds between the given date and now
3636+ const delta = timeMs - now;
3737+ const seconds = Math.round(delta / 1000);
3838+3939+ // Grab the ideal cutoff unit
4040+ const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(seconds));
4141+4242+ // units
4343+ const units = formatUnits[unitIndex];
4444+4545+ // Get the divisor to divide from the seconds. E.g. if our unit is 'day' our divisor
4646+ // is one day in seconds, so we can divide our seconds by this to get the # of days
4747+ const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
4848+4949+ // count of units
5050+ const count = Math.round(seconds / divisor);
5151+5252+ // Intl.RelativeTimeFormat do its magic
5353+ callback({
5454+ seconds: seconds,
5555+ count,
5656+ units,
5757+ text: formatter.format(count, units).replace('ago', '')
5858+ });
5959+6060+ // calculate time to next update, taking account rounding (e.g. it goes from 2 minutes to 1 minute at 90 seconds)
6161+ // and also the changeover from units (59 seconds shouldn't show as 1 minute, so at 61 seconds we schedule the next
6262+ // update for 1 second time)
6363+6464+ const divisorMs = divisor * 1000;
6565+6666+ let updateIn;
6767+6868+ if (unitIndex) {
6969+ updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs);
7070+ if (updateIn < 0) {
7171+ updateIn += divisorMs;
7272+ }
7373+ } else {
7474+ updateIn = divisorMs - (Math.abs(delta) % divisorMs);
7575+ }
7676+7777+ const updateAt = now + updateIn;
7878+7979+ return updateAt;
8080+}
···11+import { getFormatter } from './formatter';
22+import { render } from './render';
33+import type { Callback, RenderState } from './render';
44+55+interface UpdateState extends RenderState {
66+ update: number;
77+}
88+99+// keep track of each instance
1010+const instances = new Map<Object, UpdateState>();
1111+1212+// we use a single timer for efficiency and to keep updates in sync
1313+let updateInterval: number | NodeJS.Timeout;
1414+1515+// register or update instance
1616+export function register(
1717+ instance: Object,
1818+ date: Date | number,
1919+ locale: string,
2020+ live: boolean,
2121+ callback: Callback
2222+) {
2323+ // get the formatter for the given locale, we do this here so we don't keep having to look it up on each tick
2424+ const formatter = getFormatter(locale);
2525+2626+ // create state to render
2727+ const state = { date, callback, formatter };
2828+2929+ // initial render is immediate, so works for SSR
3030+ const update = render(state, Date.now());
3131+3232+ // if it's to update live, we keep a track and schedule the next update
3333+ if (live) {
3434+ instances.set(instance, { ...state, update });
3535+ } else {
3636+ instances.delete(instance);
3737+ }
3838+3939+ // start the clock ticking if there are any live instances
4040+ if (instances.size) {
4141+ updateInterval =
4242+ updateInterval ||
4343+ setInterval(() => {
4444+ const now = Date.now();
4545+ for (const state of instances.values()) {
4646+ if (state.update <= now) {
4747+ state.update = render(state, now);
4848+ }
4949+ }
5050+ }, 1000);
5151+ }
5252+}
5353+5454+export function unregister(instance: Object) {
5555+ instances.delete(instance);
5656+ if (instances.size === 0) {
5757+ clearInterval(updateInterval);
5858+ updateInterval = 0;
5959+ }
6060+}
···11+<script lang="ts">
22+ import Example from './Example.svelte';
33+</script>
44+55+# Bluesky Login
66+77+## Example
88+99+<Example />
1010+1111+## Usage
1212+1313+```svelte
1414+<script lang="ts">
1515+ import { BlueskyLogin } from '@fuxui/social';
1616+</script>
1717+1818+<BlueskyLogin login={async (handle) => {
1919+ // do login and return true if login
2020+ // is successful and should hide the modal
2121+}} />
2222+```