This episode was created in collaboration with the amazing Amanda Cavallaro.
In previous episode, we created some pure HTML+CSS mockup of a file manager. To turn it into reality, we need to rearrange them into some components.
Again, I'll be using Svelte, but you can easily try to follow the same steps using any other framework.
Structure
We'll end up with a lot more, but for now I'll have just three component classes:
App
-
Panel
(included twice) Footer
Footer
has no state.
Right now state of each Panel
s is split between App
(which contains list of files) and Panel
(which contains information which file is focused, and which are selected). This will definitely change a few times before we're done.
Events
To even see if CSS is correct, I implemented a few events:
- left clicking on a file will focus on it; it will also activate its panel if it's not active
- right clicking on a file will do all that, and also flip its selected status
App.js
The html structure is a single grid component with 4 children:
<div class="ui">
<header>
File Manager
</header>
<Panel
files={filesLeft}
position="left"
active={activePanel === "left"}
onActivate={() => activePanel = "left"}
/>
<Panel
files={filesRight}
position="right"
active={activePanel === "right"}
onActivate={() => activePanel = "right"}
/>
<Footer />
</div>
There are fancier ways to handle panel activation, but it's very simple state - either left or right panel is active, so what we have is perfectly suitable.
We pass position
to Panel
so it can have proper grid-area
. Panels don't strictly need to know if they're left or right, it just makes CSS more straightforward if they do.
Here's how we style it. I also include body styling here instead of having any kind of global css files:
<style>
:global(body) {
background-color: #226;
color: #fff;
font-family: monospace;
margin: 0;
font-size: 16px;
}
.ui {
width: 100vw;
height: 100vh;
display: grid;
grid-template-areas:
"header header"
"panel-left panel-right"
"footer footer";
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr auto;
}
.ui header {
grid-area: header;
}
header {
font-size: 24px;
margin: 4px;
}
</style>
And finally the Javascript. It's just one variable for active panel, and some static data from Cat Ipsum.
Eventually, filesLeft
and filesRight
will both come from what's actually in the filesystem, and will likely be managed elsewhere, but this is good enough for now:
<script>
import Panel from "./Panel.svelte"
import Footer from "./Footer.svelte"
let activePanel = "left"
let filesLeft = [
"Cat.js",
"ipsum.js",
"dolor.js",
"sit.js",
"amet.js",
"walk.js",
"on.js",
"keyboard.js",
"hide.js",
"when.js",
"guests.js",
"come.js",
"over.js",
"play.js",
"with.js",
"twist.js",
"ties.js",
]
let filesRight = [
"Ask.png",
"to.png",
"be.png",
"pet.png",
"then.png",
"attack.png",
"owners.png",
"hand.png",
"need.png",
"to.jpg",
"chase.png",
"tail.png",
]
</script>
Footer.svelte
Footer is completely static HTML and CSS. We'll make those buttons do things in the future, and maybe we can turn this into some kind of context-sensitive shortcuts bar. For now this will do:
<footer>
<button>F1 Help</button>
<button>F2 Menu</button>
<button>F3 View</button>
<button>F4 Edit</button>
<button>F5 Copy</button>
<button>F6 Move</button>
<button>F7 Mkdir</button>
<button>F8 Delete</button>
<button>F10 Quit</button>
</footer>
<style>
footer {
text-align: center;
grid-area: footer;
}
button {
font-family: inherit;
font-size: inherit;
background-color: #66b;
color: inherit;
}
</style>
Panel.svelte
Even to just have some mockup, we need quite a bit of state:
-
position
- left or right, just to keep CSS easy -
files
- list of files to display, passed from the parent -
active
- whether it's active or not - we need this as we need to remember focus in non-active tab, even if we don't show it -
onActivate
- callback to tell the app that this panel wants to become active -
onclick
- event handler for left clicking on a file -
onrightclick
- event handler for right clicking on a file - browser event is nonsensically named "oncontextmenu
"
<script>
export let position
export let files
export let active
export let onActivate
let focused = files[0]
let selected = []
let onclick = (file) => {
onActivate(position)
focused = file
}
let onrightclick = (file) => {
onActivate(position)
focused = file
if (selected.includes(file)) {
selected = selected.filter(f => f !== file)
} else {
selected = [...selected, file]
}
}
</script>
HTML is just a simple loop, with events for left and right clicks, and with a bunch of logic controlling CSS classes. If you're following this with a different framework, many lack shortcuts for controlling different classes with separate variables, and for preventing default event handling, so you might need to write a bit extra code:
<div class="panel {position}" class:active={active}>
{#each files as file}
<div
class="file"
class:focused={file === focused}
class:selected={selected.includes(file)}
on:click|preventDefault={() => onclick(file)}
on:contextmenu|preventDefault={() => onrightclick(file)}
>{file}</div>
{/each}
</div>
CSS is really easy. As I mentioned before:
<style>
.panel-left {
grid-area: panel-left;
}
.panel-right {
grid-area: panel-right;
}
.panel {
background: #338;
margin: 4px;
}
.file {
cursor: pointer;
}
.file.selected {
color: #ff2;
font-weight: bold;
}
.panel.active .file.focused {
background-color: #66b;
}
</style>
Result
Here's the results, looking just as our static mockup:
OK, that was a lot. From now on, we'll try to work on one thing at a time.
As usual, all the code for the episode is here.
Top comments (0)