diff --git a/.changeset/nine-pillows-melt.md b/.changeset/nine-pillows-melt.md new file mode 100644 index 00000000..186569a8 --- /dev/null +++ b/.changeset/nine-pillows-melt.md @@ -0,0 +1,5 @@ +--- +"@foxone/uikit": patch +--- + +more components diff --git a/packages/uikit/.storybook/preview.js b/packages/uikit/.storybook/preview.js index 4b852330..eb3a07ba 100644 --- a/packages/uikit/.storybook/preview.js +++ b/packages/uikit/.storybook/preview.js @@ -1,15 +1,15 @@ import { app } from "@storybook/vue3"; -import { defineComponent } from "vue"; -import { createVuetify } from "vuetify"; +import { defineComponent, watchEffect } from "vue"; +import { createVuetify, useTheme } from "vuetify"; import * as components from "vuetify/components"; import * as directives from "vuetify/directives"; import { themes } from "@storybook/theming"; import ficons from "./ficons"; import "vuetify/styles"; -import "../src/styles/index.scss"; import { usePresets } from "../src/presets"; +import UIKit from "../src/index"; export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, @@ -47,6 +47,7 @@ const options = usePresets({ const vuetify = createVuetify(options); app.use(vuetify); +app.use(UIKit); export const decorators = [ (story, context) => { @@ -56,7 +57,7 @@ export const decorators = [ name: "StoryBookWrap", components: { WrappedComponent }, template: ` - + @@ -65,9 +66,13 @@ export const decorators = [ `, setup() { - const theme = context.globals.theme; + watchEffect(() => { + const theme = useTheme(); - return { theme, context }; + theme.global.name.value = context.globals.theme; + }); + + return { context }; }, }); }, diff --git a/packages/uikit/src/components/FDivider/FDivider.stories.ts b/packages/uikit/src/components/FDivider/FDivider.stories.ts index eee100df..a7dd2b34 100644 --- a/packages/uikit/src/components/FDivider/FDivider.stories.ts +++ b/packages/uikit/src/components/FDivider/FDivider.stories.ts @@ -21,4 +21,4 @@ const Template: StoryFn = (args) => ({ }); export const Default = Template.bind({}); -Default.args = { size: 8, color: "greyscale_6" }; +Default.args = { thickness: 8, color: "greyscale_6" }; diff --git a/packages/uikit/src/components/FDivider/FDivider.tsx b/packages/uikit/src/components/FDivider/FDivider.tsx index df5da113..d82f022d 100644 --- a/packages/uikit/src/components/FDivider/FDivider.tsx +++ b/packages/uikit/src/components/FDivider/FDivider.tsx @@ -1,29 +1,12 @@ -import { computed, defineComponent } from "vue"; +import { defineComponent } from "vue"; import { VDivider } from "vuetify/components"; -import { convertToUnit } from "vuetify/lib/util/helpers.mjs"; export const FDivider = defineComponent({ name: "FDivider", - props: { - color: { - type: String, - default: "greyscale_6", - }, - size: { - type: [String, Number], - default: 1, - }, - }, - - setup(props) { - const styles = computed(() => { - return { - borderWidth: `${convertToUnit(props.size)} 0 0 0`, - borderColor: `rgb(var(--v-theme-${props.color}))`, - }; - }); + setup() { + const preset = { color: "greyscale_6", thickness: 1 }; - return () => ; + return () => ; }, }); diff --git a/packages/uikit/src/components/FSwitch/FSwitch.scss b/packages/uikit/src/components/FSwitch/FSwitch.scss index e69de29b..4bf33972 100644 --- a/packages/uikit/src/components/FSwitch/FSwitch.scss +++ b/packages/uikit/src/components/FSwitch/FSwitch.scss @@ -0,0 +1,24 @@ +.f-switch.v-switch { + &.f-switch--outlined { + .v-selection-control--dirty { + .v-switch__track { + background: rgb(var(--v-theme-greyscale_1)); + border: none; + } + + .v-switch__thumb { + color: rgb(var(--v-theme-greyscale_7)) !important; + } + } + + .v-switch__thumb { + color: rgb(var(--v-theme-greyscale_1)); + } + + .v-switch__track { + background: transparent; + border: 2px solid currentColor; + opacity: 1; + } + } +} diff --git a/packages/uikit/src/components/FSwitch/FSwitch.stories.ts b/packages/uikit/src/components/FSwitch/FSwitch.stories.ts index 29f72028..6cd4ff73 100644 --- a/packages/uikit/src/components/FSwitch/FSwitch.stories.ts +++ b/packages/uikit/src/components/FSwitch/FSwitch.stories.ts @@ -1,3 +1,4 @@ +import { ref } from "vue"; import { FSwitch } from "./FSwitch"; import { Meta, StoryFn } from "@storybook/vue3"; @@ -9,10 +10,17 @@ export default { const Template: StoryFn = (args) => ({ components: { FSwitch }, setup() { - return { args }; + const switch1 = ref(false); + + return { args, switch1 }; }, - template: ``, + template: ` +
+ + {{switch1}} +
+ `, }); export const Default = Template.bind({}); -Default.bind({}); +Default.args = { outlined: true }; diff --git a/packages/uikit/src/components/FSwitch/FSwitch.tsx b/packages/uikit/src/components/FSwitch/FSwitch.tsx index 6bb7fb88..fa4b20fd 100644 --- a/packages/uikit/src/components/FSwitch/FSwitch.tsx +++ b/packages/uikit/src/components/FSwitch/FSwitch.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from "vue"; +import { defineComponent, computed } from "vue"; import { VSwitch } from "vuetify/components"; import "./FSwitch.scss"; @@ -6,7 +6,19 @@ import "./FSwitch.scss"; export const FSwitch = defineComponent({ name: "FSwitch", - setup() { - return () => ; + props: { + outlined: { + type: Boolean, + default: true, + }, + }, + + setup(props) { + const preset = { inset: true, ripple: false, hideDetails: true }; + const classes = computed(() => { + return ["f-switch", { "f-switch--outlined": props.outlined }]; + }); + + return () => ; }, }); diff --git a/packages/uikit/src/components/FSwitch/index.ts b/packages/uikit/src/components/FSwitch/index.ts new file mode 100644 index 00000000..fd2904fe --- /dev/null +++ b/packages/uikit/src/components/FSwitch/index.ts @@ -0,0 +1 @@ +export { FSwitch } from "./FSwitch"; diff --git a/packages/uikit/src/components/FTabs/FTabs.scss b/packages/uikit/src/components/FTabs/FTabs.scss new file mode 100644 index 00000000..18280c62 --- /dev/null +++ b/packages/uikit/src/components/FTabs/FTabs.scss @@ -0,0 +1,26 @@ +.f-tabs.v-tabs { + &.f-tabs--narrow { + .v-slide-group__content { + gap: 16px; + + .v-tab { + padding: 0; + min-width: auto; + } + } + } + + .v-tab { + font-weight: 500; + font-size: 1rem; + line-height: 1.1875rem; + + .v-btn__overlay { + display: none; + } + } + + .v-tab__slider { + border-radius: 2px; + } +} \ No newline at end of file diff --git a/packages/uikit/src/components/FTabs/FTabs.stories.ts b/packages/uikit/src/components/FTabs/FTabs.stories.ts new file mode 100644 index 00000000..498e6b4b --- /dev/null +++ b/packages/uikit/src/components/FTabs/FTabs.stories.ts @@ -0,0 +1,28 @@ +import { FTabs } from "./FTabs"; +import { VTab } from "vuetify/components"; +import { Meta, StoryFn } from "@storybook/vue3"; + +export default { + name: "FTabs", + component: FTabs, +} as Meta; + +const Template: StoryFn = (args) => ({ + components: { FTabs, VTab }, + setup() { + return { args }; + }, + template: ` + + + Supply + + + Borrow + + + `, +}); + +export const Default = Template.bind({}); +Default.args = { grow: false, narrow: false }; diff --git a/packages/uikit/src/components/FTabs/FTabs.tsx b/packages/uikit/src/components/FTabs/FTabs.tsx new file mode 100644 index 00000000..eaf0d289 --- /dev/null +++ b/packages/uikit/src/components/FTabs/FTabs.tsx @@ -0,0 +1,28 @@ +import { computed, defineComponent } from "vue"; +import { VTabs } from "vuetify/components"; +import { provideDefaults } from "vuetify/lib/composables/defaults.mjs"; + +import "./FTabs.scss"; + +export const FTabs = defineComponent({ + name: "FTabs", + + props: { + narrow: Boolean, + }, + + setup(props, { slots }) { + provideDefaults({ + VTab: { + ripple: false, + }, + }); + + const presets = { height: 52 }; + const classes = computed(() => { + return ["f-tabs", { "f-tabs--narrow": props.narrow }]; + }); + + return () => ; + }, +}); diff --git a/packages/uikit/src/components/FTabs/index.ts b/packages/uikit/src/components/FTabs/index.ts new file mode 100644 index 00000000..bb5cf004 --- /dev/null +++ b/packages/uikit/src/components/FTabs/index.ts @@ -0,0 +1 @@ +export { FTabs } from "./FTabs"; diff --git a/packages/uikit/src/components/FToast/FToast.scss b/packages/uikit/src/components/FToast/FToast.scss new file mode 100644 index 00000000..3c589718 --- /dev/null +++ b/packages/uikit/src/components/FToast/FToast.scss @@ -0,0 +1,47 @@ +.v-snackbar.f-toast { + --v-theme-surface-variant: var(--v-theme-greyscale_7); + --v-theme-on-surface-variant: var(--v-theme-greyscale_1); + + .v-snackbar__content { + font-weight: 500; + font-size: 0.875rem; + line-height: 1.0625rem; + padding: 16px; + } + + .v-snackbar__actions { + font-weight: 500; + font-size: 0.875rem; + align-self: stretch; + display: flex; + align-items: stretch; + } + + .f-toast__action { + height: 100%; + display: flex; + align-items: center; + cursor: pointer; + } +} + +.v-theme--dark.f-toast { + &.f-toast--success { + --v-theme-surface-variant: var(--v-theme-success_bg); + } + + &.f-toast--error { + --v-theme-surface-variant: var(--v-theme-error_bg); + } + + &.f-toast--warning { + --v-theme-surface-variant: var(--v-theme-warning_bg); + } +} + + +.v-theme--light.f-toast { + .v-snackbar__wrapper { + box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.1); + } +} diff --git a/packages/uikit/src/components/FToast/FToast.stories.ts b/packages/uikit/src/components/FToast/FToast.stories.ts new file mode 100644 index 00000000..a1656810 --- /dev/null +++ b/packages/uikit/src/components/FToast/FToast.stories.ts @@ -0,0 +1,51 @@ +import { ref } from "vue"; +import { FToast } from "./FToast"; +import { FButton } from "../FButton"; +import { Meta, StoryFn } from "@storybook/vue3"; +import { useToast } from "../../plugins/toast"; + +export default { + name: "FToast", + component: FToast, +} as Meta; + +const Template: StoryFn = (args) => ({ + components: { FToast, FButton }, + setup() { + const toast = ref(false); + const open = () => (toast.value = true); + + return { args, toast, open }; + }, + template: ` +
+ Open + This is a toast +
+ `, +}); + +export const Default = Template.bind({}); +Default.args = { type: "success", action: { text: "Detail" } }; + +const Template2: StoryFn = (args) => ({ + setup() { + const toast = useToast(); + const show = () => + toast.show({ + message: "This is a toast", + type: ["success", "error", "warning"][ + Math.floor(Math.random() * 3) + ] as any, + ...args, + }); + + return { show, args }; + }, + template: ` + Show + `, +}); + +export const Functional = Template2.bind({}); +Functional.args = {}; diff --git a/packages/uikit/src/components/FToast/FToast.tsx b/packages/uikit/src/components/FToast/FToast.tsx new file mode 100644 index 00000000..f25746d3 --- /dev/null +++ b/packages/uikit/src/components/FToast/FToast.tsx @@ -0,0 +1,103 @@ +import { computed, defineComponent, PropType, ref, watch } from "vue"; +import { VSnackbar, VIcon, VDivider } from "vuetify/components"; + +import "./FToast.scss"; + +export interface ToastAction { + text: string; + callback?: () => void; +} + +export const types = ["success", "warning", "error"] as const; + +export const FToast = defineComponent({ + name: "FToast", + + props: { + action: { + type: Object as PropType, + default: null, + }, + type: String as PropType, + message: String, + }, + + emits: { + destroy: () => true, + }, + + setup(props, { slots, expose, emit }) { + const visible = ref(false); + + const presets = { + variant: "flat", + location: "center center", + } as const; + + const icon = computed(() => { + const icons = { success: "$check", error: "$alert", warning: "$alert" }; + + return icons[props?.type ?? ""]; + }); + + const classes = computed(() => { + return [ + "f-toast", + { "f-toast--action": props.action }, + props.type && `f-toast--${props.type}`, + ]; + }); + + const renderMessage = () => ( +
+ {icon.value && ( + + {icon.value} + + )} + {slots.default?.() || props.message} +
+ ); + + const renderActions = () => + props.action && ( +
props.action?.callback?.()}> + + {props.action.text} + $arrow_right +
+ ); + + const show = () => (visible.value = true); + + const close = () => { + visible.value = false; + emit("destroy"); + }; + + const update = (v) => (v ? show() : close()); + + expose({ show, close }); + + watch( + () => props.message, + () => { + console.log(props); + } + ); + + return () => ( + + {{ + default: renderMessage, + actions: renderActions, + }} + + ); + }, +}); diff --git a/packages/uikit/src/components/FToast/index.ts b/packages/uikit/src/components/FToast/index.ts new file mode 100644 index 00000000..7e341f40 --- /dev/null +++ b/packages/uikit/src/components/FToast/index.ts @@ -0,0 +1 @@ +export { FToast } from "./FToast"; diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index aa3b25d0..c9e20b71 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -19,3 +19,6 @@ export * from "./FQRCode"; export * from "./FSearchInput"; export * from "./FSegmentControl"; export * from "./FSlider"; +export * from "./FSwitch"; +export * from "./FTabs"; +export * from "./FToast"; diff --git a/packages/uikit/src/index.ts b/packages/uikit/src/index.ts index 5129a726..2be65299 100644 --- a/packages/uikit/src/index.ts +++ b/packages/uikit/src/index.ts @@ -1,4 +1,23 @@ -export * from "./components"; -export * from "./presets"; - import "./styles/index.scss"; +import * as allcomponents from "./components"; +import { Toast } from "./plugins"; + +import type { App } from "vue"; +import type { ToastGlobalOptions } from "./plugins/toast"; + +export interface UIKitOptions { + components?: Record; + toast?: ToastGlobalOptions; +} + +export default function install(app: App, options: UIKitOptions = {}) { + const components = options.components || allcomponents; + + for (const key in components) { + app.component(key, components[key]); + } + + app.use(Toast, options.toast); +} + +export * from "./presets"; diff --git a/packages/uikit/src/plugins/index.ts b/packages/uikit/src/plugins/index.ts new file mode 100644 index 00000000..4c91ecee --- /dev/null +++ b/packages/uikit/src/plugins/index.ts @@ -0,0 +1 @@ +export { Toast } from "./toast"; diff --git a/packages/uikit/src/plugins/toast.ts b/packages/uikit/src/plugins/toast.ts new file mode 100644 index 00000000..4bd5ff0a --- /dev/null +++ b/packages/uikit/src/plugins/toast.ts @@ -0,0 +1,82 @@ +import { createVNode, render, getCurrentInstance, nextTick } from "vue"; +import { FToast, types, ToastAction } from "../components/FToast/FToast"; + +import type { App, VNode } from "vue"; + +export interface ToastProps { + type?: typeof types[number]; + message?: string; + action?: ToastAction; +} + +export interface ToastGlobalOptions { + location: any; + timeout: number; + [key: string]: any; +} + +export type ToastHandler = { + close: () => void; +}; + +export type Keys = "show" | "success" | "error" | "warning" | "clear"; + +export type ToastMethods = Record void>; + +export function useToast() { + const instance = getCurrentInstance()!; + + return instance.appContext.config.globalProperties.$uikit.toast; +} + +function install(app: App, globalOptions: ToastGlobalOptions) { + let instance: VNode | null = null; + + const show = (options: ToastProps = {}) => { + if (instance) { + instance.component!.exposed!.close(); + } + + nextTick(() => { + const appendTo = document.querySelector("[data-v-app]"); + const container = document.createElement("div"); + const vnode = createVNode(FToast, { + ...globalOptions, + ...options, + attach: container, + onDestroy: () => { + render(null, container); + instance = null; + appendTo?.removeChild(container); + }, + }); + + vnode.appContext = app._context!; + render(vnode, container); + appendTo?.appendChild(container); + instance = vnode; + + instance.component!.exposed!.show(); + }); + }; + + const clear = () => { + if (!instance) return; + + instance!.component!.exposed!.close(); + }; + + const toast = { show, clear }; + const properties = app.config.globalProperties; + + types.forEach((type) => { + toast[type] = (options: ToastProps) => show({ type, ...options }); + }); + + properties.$uikit = properties.$uikit || {}; + properties.$uikit.toast = toast as ToastMethods; +} + +export function Toast() {} + +Toast.install = install; diff --git a/packages/uikit/src/presets/icons.ts b/packages/uikit/src/presets/icons.ts index 7444d94a..842bf086 100644 --- a/packages/uikit/src/presets/icons.ts +++ b/packages/uikit/src/presets/icons.ts @@ -18,6 +18,7 @@ import { FIconAlert4P, FIconLink4PBold, FIconHorn4P, + FIconArrowRight4P, } from "@foxone/icons"; export const icons = { @@ -40,4 +41,5 @@ export const icons = { alert: FIconAlert4P, link_bold: FIconLink4PBold, horn: FIconHorn4P, + arrow_right: FIconArrowRight4P, }; diff --git a/packages/uikit/src/shims.d.ts b/packages/uikit/src/shims.d.ts index d2a4ce44..63619d7f 100644 --- a/packages/uikit/src/shims.d.ts +++ b/packages/uikit/src/shims.d.ts @@ -1,4 +1,5 @@ import type { VNode } from "vue"; +import { ToastMethods } from "./plugins/toast"; declare global { namespace JSX { @@ -9,3 +10,13 @@ declare global { } } } + +declare module "@vue/runtime-core" { + interface UIKit { + toast: ToastMethods; + } + + export interface ComponentCustomProperties { + $uikit: UIKit; + } +} diff --git a/packages/uikit/src/stories/icons.stories.ts b/packages/uikit/src/stories/icons.stories.ts index d37886f2..c2dfbfb2 100644 --- a/packages/uikit/src/stories/icons.stories.ts +++ b/packages/uikit/src/stories/icons.stories.ts @@ -3,7 +3,7 @@ import { StoryFn } from "@storybook/vue3"; import { FIcons } from "./FIcons"; export default { - title: "FIcons", + name: "FIcons", component: FIcons, }; diff --git a/packages/uikit/src/styles/_settings.scss b/packages/uikit/src/styles/_settings.scss index 237d55b4..13006d8c 100644 --- a/packages/uikit/src/styles/_settings.scss +++ b/packages/uikit/src/styles/_settings.scss @@ -34,6 +34,11 @@ $body-font-family: Inter; // overlay $overlay-opacity: 10%, + // switch + $switch-inset-track-height: 24px, + $switch-thumb-height: 16px, + $switch-thumb-width: 16px, + // typography $typography: ( 'h6': (