Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

is-mobile判定の追加とstyled componentへのprops渡し #119

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@ import { ThemeProvider } from 'vue-styled-components'
import HelloWorld from '@/components/HelloWorld'
import store from './store'

const useWindowResizeObserver = () => {
let lastCalled = 0
const interval = 100
const resizeHandler = () => {
const now = Date.now()
if (now - lastCalled < interval) return

store.commit.ui.setViewportWidth(window.innerWidth)
lastCalled = now
}
window.addEventListener('resize', resizeHandler)
resizeHandler()
}

export default createComponent({
name: 'App',
setup() {
useWindowResizeObserver()
return () => (
<div id="app">
<div id="app" data-is-mobile={store.getters.ui.isMobile}>
<ThemeProvider theme={store.state.app.theme}>
<HelloWorld msg="traQ" />
<div id="nav">
Expand Down
19 changes: 15 additions & 4 deletions src/components/Main/MainView/MessagesView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createComponent, SetupContext, reactive } from '@vue/composition-api'
import { ChannelId } from '@/types/entity-ids'
import store from '@/store'
import styled, { PropsWithDefaultTheme } from 'vue-styled-components'
import styled, { ThemedProps } from 'vue-styled-components'
import theme from '@/lib/theme'
import media from '@/lib/media'
import { Constructorize } from '@/types/utils'

type Props = {
channelId: ChannelId
Expand All @@ -14,7 +16,10 @@ export default createComponent({
setup(props: Props, _: SetupContext) {
return () => (
<Block>
<Header>
<Header
poyo={{
a: false
}}>
{props.channelId in store.state.entities.channels
? '#' + store.state.entities.channels[props.channelId].name
: 'メッセージビュー'}
Expand All @@ -37,14 +42,20 @@ export default createComponent({
}
})

const Header = styled.h1`
const HeaderProps = { poyo: Object as Constructorize<{ a: boolean }> }

const Header = styled('h1', HeaderProps)`
font: {
size: 30px;
weight: bold;
}
color: ${theme.accent.primary};
background-color: ${props => (props.poyo.a ? 'red' : 'blue')};
`

const Block = styled.div`
color: blue;
background-color: ${theme.accent.online};
${media.mobile`
background-color: ${theme.accent.notification}
`};
`
41 changes: 41 additions & 0 deletions src/lib/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { css, ThemedProps, Interpolation } from 'vue-styled-components'

/*
* Bootstrap Media Queries
*
* ex-small < 576px : portrait phones
* 576px <= small < 768px : landscape phones
* 768px <= medium < 992px : tablets
* 992px <= large < 1200px : desktops
* 1200px <= ex-large : large desktops
*/

export const minBreakpoints = {
small: 576,
medium: 768,
large: 992,
exLarge: 1200
}
export const maxBreakpoints = {
exSmall: minBreakpoints.small - 0.02,
small: minBreakpoints.medium - 0.02,
medium: minBreakpoints.large - 0.02,
large: minBreakpoints.exLarge - 0.02
}

export const mobileMaxBreakpoint = maxBreakpoints.small

const mobile = (query: TemplateStringsArray, ctx: Interpolation<{}>) => (
_: ThemedProps<{}>
) =>
css`
[data-is-mobile='true'] & {
${css(query, ctx)}
}
`

const media = {
mobile
}

export default media
4 changes: 2 additions & 2 deletions src/lib/theme.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PropsWithDefaultTheme, DefaultTheme } from 'vue-styled-components'
import { ThemedProps, DefaultTheme } from 'vue-styled-components'

type ThemeFunction = (props: PropsWithDefaultTheme) => string
export type ThemeFunction = (props: ThemedProps<{}>) => string
type ThemeFunctions = {
[K1 in keyof DefaultTheme]: {
[K2 in keyof DefaultTheme[K1]]: ThemeFunction
Expand Down
6 changes: 1 addition & 5 deletions src/store/root/getters.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { createGetters } from 'direct-vuex'
import { S } from './state'

export const getters = createGetters<S>()({
// count(state) {
// return state.count
// }
})
export const getters = createGetters<S>()({})
7 changes: 6 additions & 1 deletion src/store/ui/getters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { createGetters } from 'direct-vuex'
import { S } from './state'
import { mobileMaxBreakpoint } from '@/lib/media'

export const getters = createGetters<S>()({})
export const getters = createGetters<S>()({
isMobile(state) {
return state.viewportWidth <= mobileMaxBreakpoint
}
})
6 changes: 5 additions & 1 deletion src/store/ui/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { createMutations } from 'direct-vuex'
import { S } from './state'

export const mutations = createMutations<S>()({})
export const mutations = createMutations<S>()({
setViewportWidth: (state: S, width: number) => {
state.viewportWidth = width
}
})
8 changes: 6 additions & 2 deletions src/store/ui/state.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export interface S {}
export interface S {
viewportWidth: number
}

export const state: S = {}
export const state: S = {
viewportWidth: 0
}
26 changes: 26 additions & 0 deletions src/types/theme.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'vue-styled-components'

declare module 'vue-styled-components' {
// traQ固有のテーマ定義
interface DefaultTheme {
accent: {
primary: string
notification: string
online: string
}
background: {
primary: string
secondary: string
tertiary: string
}
ui: {
primary: string
secondary: string
tertiary: string
}
text: {
primary: string
secondary: string
}
}
}
13 changes: 13 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Styled componentにpropとしてオブジェクトを渡す必要があるときに用いる
*
* ex: <Header poyo={{ a: true }}>shown as red</Header>
* ...
* const HeaderProps = { poyo: Object as Constructorize<{ a: boolean }> }
* const Header = styled('h1', HeaderProps)`
* color: ${props => (props.poyo.a ? 'red' : 'blue')};
* `
*/
export type Constructorize<T> = ObjectConstructor & {
new (): T
}
93 changes: 50 additions & 43 deletions src/types/vue-styled-components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,42 @@ declare module 'vue-styled-components' {
import * as Vue from 'vue'
import { ComponentRenderProxy } from '@vue/composition-api'

export type CSSProperties = CSS.Properties<string | number>
type CSSProperties = CSS.Properties<string | number>

export type CSSPseudos = { [K in CSS.Pseudos]?: CSSObject }
type CSSPseudos = { [K in CSS.Pseudos]?: CSSObject }

export interface CSSObject extends CSSProperties, CSSPseudos {
interface CSSObject extends CSSProperties, CSSPseudos {
[key: string]: CSSObject | string | number | undefined
}

export type CSS = CSSProperties
type CSS = CSSProperties

export type StyledComponent = Vue.Component &
type StyledComponent<P> = Vue.Component<{}, {}, {}, P> &
Vue.VueConstructor & {
extend(
cssRules: TemplateStringsArray,
...interpolate: TemplateStringsArray[]
): StyledComponent
withComponent(target: Vue.VueConstructor): StyledComponent
...interpolate: Interpolation<P>[]
): StyledComponent<P>
withComponent(target: Vue.VueConstructor): StyledComponent<P>
}

export type StyledComponentElements<T = HTMLElements> = {
[k in keyof T]: (
type StyledComponentElements<T = HTMLElements> = {
[k in keyof T]: <P>(
str: TemplateStringsArray,
...args: ((props: PropsWithDefaultTheme) => string)[]
) => StyledComponent
...args: Interpolation<P>[]
) => StyledComponent<P>
}
export type Styled<T = HTMLElements> = StyledComponentElements & {
<T>(Component: T, props?: Record<string, Vue.PropOptions['type']>): (
type Styled<T = HTMLElements> = StyledComponentElements & {
<Props extends Record<string, any>>(
Component: keyof HTMLElements,
props?: Props
): (
str: TemplateStringsArray,
...args: ((props: PropsWithDefaultTheme) => string)[]
) => StyledComponent
...args: ((props: ThemedProps<TypeOfProp<Props>>) => string)[]
) => StyledComponent<Props>
}

export interface HTMLElements {
interface HTMLElements {
a: HTMLAnchorElement
abbr: HTMLElement
address: HTMLElement
Expand Down Expand Up @@ -151,38 +154,42 @@ declare module 'vue-styled-components' {
wbr: HTMLElement
}

export let styled: Styled
export default styled
export type Interpolation<P> = ((props: ThemedProps<P>) => string) | string

export interface DefaultTheme extends Theme {}
export type PropsWithDefaultTheme = {
export interface DefaultTheme {}
export type ThemeProp = {
theme: DefaultTheme
}
export type ThemedProps<P> = ThemeProp & P

export type ThemeProviderComponent = {
new (): ComponentRenderProxy<PropsWithDefaultTheme>
new (): ComponentRenderProxy<ThemeProp>
}
export const ThemeProvider: ThemeProviderComponent

export const css: <P>(
str: TemplateStringsArray,
...args: Interpolation<P>[]
) => string

export let styled: Styled
export default styled
}

// traQ固有のテーマ定義
interface Theme {
accent: {
primary: string
notification: string
online: string
}
background: {
primary: string
secondary: string
tertiary: string
}
ui: {
primary: string
secondary: string
tertiary: string
}
text: {
primary: string
secondary: string
}
type Primitify<T> = T extends String
? string
: T extends Number
? number
: T extends Boolean
? boolean
: T extends BigInt
? bigint
: T extends Symbol
? symbol
: T

type TypeOfProp<P> = {
[K in keyof P]: P[K] extends new (...args: any) => any
? Primitify<InstanceType<P[K]>>
: P[K]
}