init commit
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
*.timestamp-*-*.mjs
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"vitest.explorer",
|
||||||
|
"oxc.oxc-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Mock challenge: BaseButton component
|
||||||
|
|
||||||
|
Build a reusable Vue 3 button component suitable for a design system. Focus on API design, accessibility, and Vue 3 best practices.
|
||||||
|
|
||||||
|
**Suggested time: 25–35 minutes**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core requirements
|
||||||
|
|
||||||
|
- Accept a `variant` prop: `primary`, `secondary`, `danger` — each should have visually distinct styles
|
||||||
|
- Accept a `size` prop: `sm`, `md`, `lg` — affects padding and font size
|
||||||
|
- Accept a `disabled` prop — visually and functionally disables the button
|
||||||
|
- Accept a `loading` prop — shows a loading indicator and prevents interaction
|
||||||
|
- Use a default `slot` for button label content
|
||||||
|
- Explicitly declare a `click` emit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessibility requirements
|
||||||
|
|
||||||
|
- Use a native `<button>` element — not a `<div>`
|
||||||
|
- When `loading` is true, set `aria-busy="true"` and `aria-disabled="true"`
|
||||||
|
- When `disabled` is true, use the native `disabled` attribute (not just styling)
|
||||||
|
- Ensure visible focus styles are not removed — `outline: none` without a replacement is a fail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bonus points
|
||||||
|
|
||||||
|
- Add an optional `icon` slot for a leading icon
|
||||||
|
- Type all props with TypeScript
|
||||||
|
- Add a `full-width` prop that makes the button `width: 100%`
|
||||||
|
- Write at least one Vitest unit test — e.g. that the click event does not fire when disabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Things they'll be watching for
|
||||||
|
|
||||||
|
- Do you use `defineProps` and `defineEmits` correctly with script setup syntax?
|
||||||
|
- Do you use a `computed` to derive CSS classes from props, or inline ternaries everywhere?
|
||||||
|
- Do you handle the loading state gracefully — preventing double-clicks, communicating state to screen readers?
|
||||||
|
- Do you narrate your thinking as you go?
|
||||||
|
|
||||||
|
> **Tip:** Before writing any code, spend 1–2 minutes talking through your approach out loud. Interviewers value reasoning over speed.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# vue-practice
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Recommended Browser Setup
|
||||||
|
|
||||||
|
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||||
|
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||||
|
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||||
|
- Firefox:
|
||||||
|
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||||
|
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm test:unit
|
||||||
|
```
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vite App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-practice",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"format": "oxfmt src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.32",
|
||||||
|
"vue-router": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node24": "^24.0.4",
|
||||||
|
"@types/jsdom": "^28.0.1",
|
||||||
|
"@types/node": "^24.12.2",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"jsdom": "^29.0.2",
|
||||||
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"oxfmt": "^0.45.0",
|
||||||
|
"typescript": "~6.0.0",
|
||||||
|
"vite": "^8.0.8",
|
||||||
|
"vite-plugin-vue-devtools": "^8.1.1",
|
||||||
|
"vitest": "^4.1.4",
|
||||||
|
"vue-tsc": "^3.2.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3096
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
+41
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div class="button-group">
|
||||||
|
|
||||||
|
<MyButton variant="primary"
|
||||||
|
size="sm">
|
||||||
|
Primary Small
|
||||||
|
</MyButton>
|
||||||
|
<MyButton variant="secondary"
|
||||||
|
size="md">
|
||||||
|
Secondary Medium
|
||||||
|
</MyButton>
|
||||||
|
<MyButton variant="danger"
|
||||||
|
size="lg">
|
||||||
|
Danger Large
|
||||||
|
</MyButton>
|
||||||
|
|
||||||
|
<MyButton variant="danger"
|
||||||
|
size="lg"
|
||||||
|
:loading="true">
|
||||||
|
Danger Large
|
||||||
|
</MyButton>
|
||||||
|
|
||||||
|
<MyButton variant="danger"
|
||||||
|
size="lg"
|
||||||
|
:disabled="true">
|
||||||
|
Danger Large
|
||||||
|
</MyButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MyButton from './components/MyButton.vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import App from '../App.vue'
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('mounts renders properly', () => {
|
||||||
|
const wrapper = mount(App)
|
||||||
|
expect(wrapper.text()).toContain('You did it!')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<button class="btn"
|
||||||
|
type="button"
|
||||||
|
:aria-busy="loading"
|
||||||
|
:class="classes"
|
||||||
|
:disabled="disabled || loading">
|
||||||
|
<template v-if="loading">
|
||||||
|
Loading...
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<span v-if="icon">
|
||||||
|
<!-- icon placeholder -->
|
||||||
|
</span>
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
disabled?: boolean
|
||||||
|
icon?: string
|
||||||
|
loading?: boolean
|
||||||
|
size: 'sm' | 'md' | 'lg'
|
||||||
|
variant: 'primary' | 'secondary' | 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant = "primary",
|
||||||
|
size = "md",
|
||||||
|
disabled = false,
|
||||||
|
loading = false
|
||||||
|
} = defineProps<ButtonProps>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['click'])
|
||||||
|
|
||||||
|
const classes = computed(() => [
|
||||||
|
`btn--${variant}`,
|
||||||
|
`btn--${size}`
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn {
|
||||||
|
--background-color: navy;
|
||||||
|
--border-radius: 0.25rem;
|
||||||
|
--cursor-type: pointer;
|
||||||
|
--text-color: white;
|
||||||
|
--text-size: 1rem;
|
||||||
|
--horizontal-padding: 0.75rem;
|
||||||
|
--vertical-padding: 0.5rem;
|
||||||
|
|
||||||
|
background: var(--background-color);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: var(--cursor-type);
|
||||||
|
font-size: var(--text-size);
|
||||||
|
padding-block: var(--vertical-padding);
|
||||||
|
padding-inline: var(--horizontal-padding);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px dashed CanvasText;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn--secondary {
|
||||||
|
--background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn--danger {
|
||||||
|
--background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn--sm {
|
||||||
|
--border-radius: 0.15rem;
|
||||||
|
--text-size: 0.825rem;
|
||||||
|
--vertical-padding: 0.25rem;
|
||||||
|
--horizontal-padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn--lg {
|
||||||
|
--text-size: 1.25rem;
|
||||||
|
--vertical-padding: 0.75rem;
|
||||||
|
--horizontal-padding: 1rem;
|
||||||
|
--border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-busy="true"] {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
--background-color: gray;
|
||||||
|
--cursor-type: not-allowed;
|
||||||
|
--text-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Extra safety for array and object lookups, but may have false positives.
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Path mapping for cleaner imports.
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.vitest.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node24/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||||
|
// Bundler mode provides a smoother developer experience.
|
||||||
|
"module": "preserve",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
|
||||||
|
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||||
|
"types": ["node"],
|
||||||
|
|
||||||
|
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.app.json",
|
||||||
|
|
||||||
|
// Override to include only test files and clear exclusions.
|
||||||
|
// Application code imported in tests is automatically included via module resolution.
|
||||||
|
"include": ["src/**/__tests__/*", "env.d.ts"],
|
||||||
|
"exclude": [],
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
// Vitest runs in a different environment than the application code.
|
||||||
|
// Adjust lib and types accordingly.
|
||||||
|
"lib": [],
|
||||||
|
"types": ["node", "jsdom"],
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||||
|
import viteConfig from './vite.config'
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
viteConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||||
|
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user