Skip to content

Instantly share code, notes, and snippets.

@scriptype
Last active July 21, 2021 10:18
Show Gist options
  • Save scriptype/80ff816c6ebd9faefbb3 to your computer and use it in GitHub Desktop.
Save scriptype/80ff816c6ebd9faefbb3 to your computer and use it in GitHub Desktop.
Javascript Sudoku Puzzle Generator
<!--
# Javascript Sudoku Puzzle Generator
Generation process is handled in a producer wrapped in
a worker that is embedded in html.
API is designed to be pluggable, but needs more
interface to support this. Currently it responds
with an array of rows.
JS code includes:
- a utility object,
- an adapter that acts as the communication layer
between producer and consumer (app)
- app singleton that includes the rendering logic
-->
<div id='sudoku-app'></div>
<!-- https://github.com/scriptype/bem -->
<script type='text/javascript'>
"use strict";!function(a){if("function"==typeof bootstrap)bootstrap("bem",a);else if("object"==typeof exports&&"object"==typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define(a);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeBem=a}else{if("undefined"==typeof window&&"undefined"==typeof self)throw new Error("This environment was not anticipated by bem. Please file a bug.");var b="undefined"!=typeof window?window:self,c=b.bem;b.bem=a(),b.bem.noConflict=function(){return b.bem=c,this}}}(function(){function a(a){"undefined"!=typeof a.modifier&&(c.modifier=a.modifier),"undefined"!=typeof a.element&&(c.element=a.element)}function b(a){if(!d.validate(a))return null;var b=a.block,e=a.element,f=a.modifiers,g=b,h=[];return!!e&&(g+=""+c.element+e),!!f&&Object.keys(f).forEach(function(a){var d=f[a],i="function"==typeof d?d(b,e,f):d;!!i&&h.push(""+g+c.modifier+a+" ")}),(g+" "+h.join("")).slice(0,-1)}var c={element:"__",modifier:"--"},d={messages:{block:"You must specify the name of block.",element:"Element name must be a string.",modifier:"Modifiers must be supplied in the `{name : bool || fn}` style."},blockName:function(a){return"undefined"!=typeof a&&"string"==typeof a&&a.length?!0:(console.warn(this.messages.block),!1)},element:function(a){return"undefined"!=typeof a&&"string"!=typeof a?(console.warn(this.messages.element),!1):!0},modifiers:function(a){return"undefined"==typeof a||"object"==typeof a&&"[object Object]"===toString.call(a)?!0:(console.warn(this.messages.modifier),!1)},validate:function(a){return this.blockName(a.block)&&this.element(a.element)&&this.modifiers(a.modifiers)}};return{setDelimiters:a,makeClassName:b}});
</script>
<!-- Include Babel to transform code in browser -->
<script type='text/javascript'
src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser-polyfill.min.js'>
</script>
<script type='text/javascript'
src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser.min.js'>
</script>
<!-- worker, sudoku api -->
<script type='text/babel' id='worker'>
self.sudoku = null
// Worker Setup
self.addEventListener('message', (event) => {
var options = JSON.parse(event.data);
switch (options.method) {
case 'generate':
var { hints, limit } = options
self.sudoku = new Sudoku(hints, limit).generate()
self.postMessage({
success: self.sudoku.success,
board: self.sudoku.getBoard(),
solution: self.sudoku.getSolution()
});
break;
case 'validate':
var { map, number, index } = options
self.postMessage({
result: sudoku.validate(map, number, index)
});
break;
}
}, false);
// API
class Sudoku {
constructor(hints, limit) {
this.hints = hints
this.limit = limit || 10000
this._logs = {
raw: [],
incidents: {
limitExceeded: 0,
notValid: 0,
noNumbers: 0
}
}
this.success = null
this.numbers = () =>
new Array(9)
.join(" ")
.split(" ")
.map((num , i) => i + 1)
/*
Will be used in initial map. Each row will be
consisted of randomly ordered numbers
*/
this.randomRow = () => {
var row = []
var numbers = this.numbers()
while (row.length < 9) {
var index = Math.floor(Math.random() * numbers.length)
row.push(numbers[index])
numbers.splice(index, 1)
}
return row
}
/*
This is the dummy placeholder for the
final results. Will be overridden through the
backtracking process, and at the and, this will
be the real results.
*/
this.result = new Array(9 * 9)
.join(" ")
.split(" ")
.map(entry => null)
/*
Will be used as the nodeTree in the
process of backtracking. Each cell has 9 alternative
paths (randomly ordered).
*/
this.map = new Array(9 * 9)
.join(" ")
.split(" ")
.map(path => this.randomRow())
/*
Will be used as history in the backtracking
process for checking if a candidate number is valid.
*/
this.stack = []
return this
}
toRows(arr) {
var row = 0
var asRows = new Array(9)
.join(" ")
.split(" ")
.map(row => [])
for (let [index, entry] of arr.entries()) {
asRows[row].push(entry)
if ( !((index + 1) % 9) ) {
row += 1
}
}
return asRows
}
no(path, index, msg) {
var number = path[path.length - 1]
this._logs.raw.push(`no: @${index} [${number}] ${msg} ${path} `)
}
yes(path, index) {
this._logs.raw.push(`yes: ${index} ${path}`)
}
finalLog() {
console.groupCollapsed('Raw Logs')
console.groupCollapsed(this._logs.raw)
console.groupEnd()
console.groupEnd()
console.groupCollapsed('Incidents')
console.groupCollapsed(this._logs.incidents)
console.groupEnd()
console.groupEnd()
}
getBoard() {
return this.toRows(this.substractCells())
}
getSolution() {
return this.toRows(this.result)
}
substractCells() {
var _getNonEmptyIndex = () => {
var index = Math.floor(Math.random() * _result.length)
return _result[index] ? index : _getNonEmptyIndex()
}
var _result = this.result.filter(() => true)
while (
_result.length - this.hints >
_result.filter(n => !n).length
) {
_result[_getNonEmptyIndex()] = ''
}
return _result
}
validate(map, number, index) {
var rowIndex = Math.floor(index / 9)
var colIndex = index % 9
var row = map.slice(
rowIndex * 9, 9 * (rowIndex + 1)
)
var col = map.filter((e, i) =>
i % 9 === colIndex
)
var boxRow = Math.floor(rowIndex / 3)
var boxCol = Math.floor(colIndex / 3)
var box = map.filter((e, i) =>
Math.floor(Math.floor(i / 9) / 3) === boxRow &&
Math.floor((i % 9) / 3) === boxCol
)
return {
row: {
first: row.indexOf(number),
last: row.lastIndexOf(number)
},
col: {
first: col.indexOf(number),
last: col.lastIndexOf(number)
},
box: {
first: box.indexOf(number),
last: box.lastIndexOf(number)
}
}
}
_validate(map, index) {
if (!map[index].length) {
return false
}
this.stack.splice(index, this.stack.length)
var path = map[index]
var number = path[path.length - 1]
var didFoundNumber = this.validate(this.stack, number, index)
return (
didFoundNumber.col.first === -1 &&
didFoundNumber.row.first === -1 &&
didFoundNumber.box.first === -1
)
}
_generate(map, index) {
if (index === 9 * 9) {
return true
}
if (--this.limit < 0) {
this._logs.incidents.limitExceeded++
this.no(map[index], index, 'limit exceeded')
return false
}
var path = map[index]
if (!path.length) {
map[index] = this.numbers()
map[index - 1].pop()
this._logs.incidents.noNumbers++
this.no(path, index, 'no numbers in it')
return false
}
var currentNumber = path[path.length - 1]
var isValid = this._validate(map, index)
if (!isValid) {
map[index].pop()
map[index + 1] = this.numbers()
this._logs.incidents.notValid++
this.no(path, index, 'is not valid')
return false
} else {
this.stack.push(currentNumber)
}
for (let number of path.entries()) {
if (this._generate(map, index + 1)) {
this.result[index] = currentNumber
this.yes(path, index)
return true
}
}
return false
}
generate() {
if (this._generate(this.map, 0)) {
this.success = true
}
this.finalLog()
return this
}
}
</script>

Javascript Sudoku Puzzle Generator

In this demo, backtracking algorithm is used for generating the sudoku. Difficulty and solvability is totally random as I randomly left a number of hints from a full-filled board.

A Pen by Mustafa Enes on CodePen.

License.

// Utility
var utils = (() => {
function dom (selector) {
if (selector[0] === '#') {
return document.getElementById(selector.slice(1))
}
return document.querySelectorAll(selector)
}
function copyJSON (obj) {
return JSON.parse(JSON.stringify(obj))
}
function isTouchDevice () {
return navigator.userAgent
.match(/(iPhone|iPod|iPad|Android|BlackBerry)/)
}
function getWorkerURLFromElement(selector) {
var element = dom(selector)
var content = babel.transform(element.innerText).code
var blob = new Blob([content], {type: 'text/javascript'})
return URL.createObjectURL(blob)
}
// Will be used for restoring caret positions on rerenders.
// Taken from:
// http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
var cursorManager = (function () {
var cursorManager = {}
var voidNodeTags = [
'AREA', 'BASE', 'BR', 'COL', 'EMBED',
'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK',
'MENUITEM', 'META', 'PARAM', 'SOURCE',
'TRACK', 'WBR', 'BASEFONT', 'BGSOUND',
'FRAME', 'ISINDEX'
];
Array.prototype.contains = function(obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
}
function canContainText(node) {
if(node.nodeType == 1) {
return !voidNodeTags.contains(node.nodeName);
} else {
return false;
}
};
function getLastChildElement(el){
var lc = el.lastChild;
while(lc && lc.nodeType != 1) {
if(lc.previousSibling)
lc = lc.previousSibling;
else
break;
}
return lc;
}
cursorManager.setEndOfContenteditable = function(contentEditableElement) {
while(getLastChildElement(contentEditableElement) &&
canContainText(getLastChildElement(contentEditableElement))) {
contentEditableElement = getLastChildElement(contentEditableElement);
}
var range,selection;
if(document.createRange) {
range = document.createRange();
range.selectNodeContents(contentEditableElement);
range.collapse(false);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
else if(document.selection)
{
range = document.body.createTextRange();
range.moveToElementText(contentEditableElement);
range.collapse(false);
range.select();
}
}
return cursorManager
})()
return {
copyJSON, cursorManager, dom,
getWorkerURLFromElement, isTouchDevice
}
})();
// API Adapter
class SudokuAdapter {
constructor(url) {
this.worker = new Worker(url)
return this
}
_postMessage(options) {
this.worker.postMessage(JSON.stringify(options))
return new Promise((resolve, reject) => {
this.worker.onmessage = (event) => {
resolve(event.data)
}
})
}
generate(options) {
options = Object.assign
({}, options, { method: 'generate' })
return this._postMessage(options)
}
validate(options) {
options = Object.assign
({}, options, { method: 'validate' })
return this._postMessage(options)
}
}
// Client Side Settings
const SUDOKU_APP_CONFIG = {
HINTS: 34,
TRY_LIMIT: 100000,
WORKER_URL: utils.getWorkerURLFromElement('#worker'),
DOM_TARGET: utils.dom('#sudoku-app')
}
// Client Side
var SudokuApp = (config => {
const {
HINTS, TRY_LIMIT,
WORKER_URL, DOM_TARGET
} = config
var sudokuAdapter = new SudokuAdapter(WORKER_URL)
var state = {
success: null,
board: null,
solution: null,
solved: null,
errors: []
};
Object.observe(state, render)
var history = [state]
var historyStash = []
// Event listeners
var onClickGenerate = initialize
var onClickSolve = function () {
setState({
board: state.solution,
solved: true,
errors: []
})
}
var onKeyUpCell = function (event) {
var copy = (obj) => JSON.parse(
JSON.stringify(obj)
)
var key = event.keyCode
if ( // a
key === 36 || // r
key === 37 || // r
key === 38 || // o
key === 39 || // w
key === 9 || // tab
// mod key flags are always false in keyup event
// keyIdentifier doesn't seem to be implemented
// in all browsers
key === 17 || // Control
key === 16 || // Shift
key === 91 || // Meta
key === 19 || // Alt
event.keyIdentifier === 'Control' ||
event.keyIdentifier === 'Shift' ||
event.keyIdentifier === 'Meta' ||
event.keyIdentifier === 'Alt'
) return
var cell = event.target
var value = cell.innerText
if (value.length > 4) {
cell.innerText = value.slice(0, 4)
return false
}
var cellIndex = cell.getAttribute('data-cell-index')
cellIndex = parseInt(cellIndex, 10)
var rowIndex = Math.floor(cellIndex / 9)
var cellIndexInRow = cellIndex - (rowIndex * 9)
var board = Object.assign([], state.board)
board[rowIndex].splice(cellIndexInRow, 1, value)
validate(board).then(results => {
historyStash = []
history.push({})
setState({
board: board,
errors: results
}, (newState) => {
history[history.length - 1] = newState
restoreCaretPosition(cellIndex)
})
})
}
function keyDown (event) {
var keys = {
ctrlOrCmd: event.ctrlKey || event.metaKey,
shift: event.shiftKey,
z: event.keyCode === 90
}
if (keys.ctrlOrCmd && keys.z) {
if (keys.shift && historyStash.length) {
redo()
} else if (!keys.shift && history.length > 1) {
undo()
}
}
}
function undo () {
historyStash.push(history.pop())
setState(utils.copyJSON(history[history.length - 1]))
}
function redo () {
history.push(historyStash.pop())
setState(utils.copyJSON(history[history.length - 1]))
}
function initialize () {
unbindEvents()
render()
getSudoku().then(sudoku => {
setState({
success: sudoku.success,
board: sudoku.board,
solution: sudoku.solution,
errors: [],
solved: false
}, (newState) => {
history = [newState]
historyStash = []
})
})
}
function setState(newState, callback) {
requestAnimationFrame(() => {
Object.assign(state, newState)
if (typeof callback === 'function') {
var param = utils.copyJSON(state)
requestAnimationFrame(callback.bind(null, param))
}
})
}
function bindEvents() {
var generateButton = utils.dom('#generate-button')
var solveButton = utils.dom('#solve-button')
var undoButton = utils.dom('#undo-button')
var redoButton = utils.dom('#redo-button')
generateButton &&
generateButton
.addEventListener('click', onClickGenerate)
solveButton &&
solveButton
.addEventListener('click', onClickSolve)
undoButton &&
undoButton
.addEventListener('click', undo)
redoButton &&
redoButton
.addEventListener('click', redo)
var cells = utils.dom('.sudoku__table-cell')
;[].forEach.call(cells, (cell) => {
cell.addEventListener('keyup', onKeyUpCell)
})
window.addEventListener('keydown', keyDown)
}
function unbindEvents() {
var generateButton = utils.dom('#generate-button')
var solveButton = utils.dom('#solve-button')
var undoButton = utils.dom('#undo-button')
var redoButton = utils.dom('#redo-button')
generateButton &&
generateButton
.removeEventListener('click', onClickGenerate)
solveButton &&
solveButton
.removeEventListener('click', onClickSolve)
undoButton &&
undoButton
.removeEventListener('click', undo)
redoButton &&
redoButton
.removeEventListener('click', redo)
var cells = utils.dom('.sudoku__table-cell')
;[].forEach.call(cells, (cell) => {
cell.removeEventListener('keyup', onKeyUpCell)
})
window.removeEventListener('keydown', keyDown)
}
function restoreCaretPosition(cellIndex) {
utils.cursorManager.setEndOfContenteditable(
utils.dom(`[data-cell-index="${ cellIndex }"]`)[0]
)
}
function getSudoku() {
return sudokuAdapter.generate({
hints: HINTS,
limit: TRY_LIMIT
})
}
function validate(board) {
var map = board.reduce((memo, row) => {
for (let num of row) {
memo.push(num)
}
return memo
}, []).map((num) => parseInt(num, 10))
var validations = []
// Will validate one by one
for (let [index, number] of map.entries()) {
if (!number) {
validations.push(
new Promise(res => {
res({ result: { box: -1, col: -1, row: -1 } })
})
)
} else {
let all = Promise.all(validations)
validations.push(all.then(() => {
return sudokuAdapter.validate({map, number, index})
}))
}
}
return Promise.all(validations)
.then(values => {
var errors = []
for (let [index, validation] of values.entries()) {
let { box, col, row } = validation.result
let errorInBox = box.first !== box.last
let errorInCol = col.first !== col.last
let errorInRow = row.first !== row.last
let indexOfRow = Math.floor(index / 9)
let indexInRow = index - (indexOfRow * 9)
errors[index] = errorInRow || errorInCol || errorInBox
}
return errors
})
}
function render() {
unbindEvents()
DOM_TARGET.innerHTML = `
<div class='sudoku'>
${ headerComponent() }
${ contentComponent() }
</div>
`
bindEvents()
}
function buttonComponent(props) {
var { id, text, mods, classes } = props
var blockName = 'button'
var modifiers = {}
var modType = toString.call(mods)
if (modType === '[object String]') {
modifiers[mods] = true
} else if (modType === '[object Array]') {
for (let [index, modName] of mods.entries()) {
modifiers[modName] = true
}
}
var blockClasses = bem.makeClassName({
block: blockName,
modifiers: modifiers
});
var buttonTextClass = `${blockName}-text`
if (Object.keys(modifiers).length) {
buttonTextClass += (
Object.keys(modifiers).reduce((memo, curr) => {
return memo + ` ${blockName}--${curr}-text`
}, '')
)
}
var lgText = typeof text === 'string' ?
text : text[0]
var mdText = typeof text === 'string' ?
text : text[1]
var smText = typeof text === 'string' ?
text : text[2]
return (`
<button
id='${ id }'
class='${ blockClasses } ${ classes }'>
<span class='show-on-sm ${buttonTextClass}'>
${ smText }
</span>
<span class='show-on-md ${buttonTextClass}'>
${ mdText }
</span>
<span class='show-on-lg ${buttonTextClass}'>
${ lgText }
</span>
</button>
`)
}
function messageComponent(options) {
var { state, content } = options
var messageClass = bem.makeClassName({
block: 'message',
modifiers: state ? {
[state]: true
} : {}
})
return (`
<p class='${ messageClass }'>
${ content }
</p>
`)
}
function descriptionComponent(options) {
var { className, infoLevel } = options
var technical = `
In this demo,
<a href='https://en.wikipedia.org/wiki/Backtracking'>
backtracking algorithm
</a> is used for <em>generating</em>
the sudoku.`
var description = `
Difficulty and solvability is
totally random as I randomly left a certain number of hints
from a full-filled board.
`
if (infoLevel === 'full') {
return (`
<p class='${ className || '' }'>
${ technical } ${ description }
</p>
`)
} else if (infoLevel === 'mini') {
return (`
<p class='${ className || '' }'>
${ description }
</p>
`)
}
}
function restoreScrollPosComponent() {
return `<div style='height: 540px'></div>`
}
function headerComponent() {
return (`
<div class='sudoku__header'>
<h1 class='sudoku__title'>
<span class='show-on-sm'>
Sudoku
</span>
<span class='show-on-md'>
Sudoku Puzzle
</span>
<span class='show-on-lg'>
Javascript Sudoku Puzzle Generator
</span>
</h1>
${descriptionComponent({
infoLevel: 'mini',
className: 'sudoku__description show-on-md'
})}
${descriptionComponent({
infoLevel: 'full',
className: 'sudoku__description show-on-lg'
})}
${
state.success ? (`
${buttonComponent({
id: 'generate-button',
text: ['New Board', 'New Board', 'New'],
mods: 'primary'
})}
${ state.solved ?
buttonComponent({
id: 'solve-button',
text: 'Solved',
mods: 'tertiary'
}) :
buttonComponent({
id: 'solve-button',
text: 'Solve',
mods: 'secondary'
})
}
`)
: (`
${buttonComponent({
id: 'generate-button',
text: ['Generating', '', ''],
mods: ['disabled', 'loading']
})}
${buttonComponent({
id: 'solve-button',
text: 'Solve',
mods: 'disabled'
})}
`)
}
${ utils.isTouchDevice() ? (`
${buttonComponent({
id: 'redo-button',
text: ['&raquo;', '&raquo;', '&gt;', '&gt;'],
classes: 'fr',
mods: [
'neutral',
'compound',
'compound-last',
`${ !historyStash.length ?
'disabled' :
''
}`
]
})}
${buttonComponent({
id: 'undo-button',
text: ['&laquo;', '&laquo;', '&lt;', '&lt;'],
classes: 'fr',
mods: [
'neutral',
'compound',
'compound-first',
`${ history.length > 1 ?
'' :
'disabled'
}`
]
})}
`) : ''}
</div>
`)
}
function contentComponent() {
var _isSeparator = (index) =>
!!index && !((index + 1) % 3)
var resultReady = !!state.board
var fail = resultReady && !state.success
if (!resultReady) {
return (`
${messageComponent({
state: 'busy',
content: `Generating new board...`
})}
${ restoreScrollPosComponent() }
`)
}
if (fail) {
return (`
${messageComponent({
state: 'fail',
content: `Something went wrong with this board, try generating another one.`
})}
${ restoreScrollPosComponent() }
`)
}
var rows = state.board
return (`
<table class='sudoku__table'>
${rows.map((row, index) => {
let className = bem.makeClassName({
block: 'sudoku',
element: 'table-row',
modifiers: {
separator: _isSeparator(index)
}
});
return (
`<tr class='${ className }'>
${row.map((num, _index) => {
let cellIndex = (index * 9) + _index
let separator = _isSeparator(_index)
let editable = typeof num !== 'number'
let error = state.errors[cellIndex]
let className = bem.makeClassName({
block: 'sudoku',
element: 'table-cell',
modifiers: {
separator,
editable,
error,
'editable-error': editable && error
}
});
return (
`\n\t
<td class='${ className }'
data-cell-index='${ cellIndex }'
${ editable ? 'contenteditable' : '' }>
${ num }
</td>`
)
}).join('')}
\n</tr>\n`
)
}).join('')}
</table>
`)
}
return { initialize }
})( SUDOKU_APP_CONFIG ).initialize()
/**********************************\
--------- Sudoku App Styles --------
Contents:
- Global definitions
- Modules
Each module may have its own
variables, mixins, animations and
media queries.
This is the convention followed
in structuring rules
// ==================
// Foo
// ==================
// Variables (Variables of Foo)
[...]
// Lorem (Lorem of foo)
[...]
// ==================
// Bar
// ==================
[...]
\**********************************/
// ==================
// Global
// ==================
// Fonts
@font-face {
src: url("http://enes.in/GillSansTr-LightNr.otf");
font-family: Gill;
font-weight: 100
}
@font-face {
src: url("http://enes.in/GillSansTr-Normal.otf");
font-family: Gill;
font-weight: 300
}
@font-face {
src: url("http://enes.in/GillSansTr-Bold.otf");
font-family: Gill;
font-weight: 600
}
@font-face {
src: url("http://enes.in/GillSansTr-ExtraBold.otf");
font-family: Gill;
font-weight: 700
}
@font-face {
src: url("http://enes.in/GillSansTr-UltraBold.otf");
font-family: Gill;
font-weight: 900
}
html, body {
width: 100%;
height: 100%
}
body {
margin: 0;
background: #f0f0f0
}
// Variables
$g-transition-duration: .2s;
$g-breakpoint-xs: 260px;
$g-breakpoint-sm: 420px;
$g-breakpoint-md: 615px;
// Responsive
@media (max-width: $g-breakpoint-xs) {
.show-on-sm { display: none; }
.show-on-md { display: none; }
.show-on-lg { display: none; }
.show-on-xs { display: block; }
}
@media (max-width: $g-breakpoint-sm) {
.show-on-xs { display: none; }
.show-on-md { display: none; }
.show-on-lg { display: none; }
.show-on-sm { display: block; }
}
@media (min-width: $g-breakpoint-sm + 1)
and (max-width: $g-breakpoint-md) {
.show-on-xs { display: none; }
.show-on-sm { display: none; }
.show-on-lg { display: none; }
.show-on-md { display: block; }
}
@media (min-width: $g-breakpoint-md) {
.show-on-xs { display: none; }
.show-on-sm { display: none; }
.show-on-md { display: none; }
.show-on-lg { display: block; }
}
// Animations
@keyframes progress {
0% {
box-shadow: none;
}
25% {
box-shadow: 2px -2px 0 1px;
}
50% {
box-shadow: 2px -2px 0 1px,
7px -2px 0 1px;
}
100% {
box-shadow: 2px -2px 0 1px,
7px -2px 0 1px,
12px -2px 0 1px;
}
}
.fr { float: right; }
.fl { float: left; }
// ==================
// Modules
// ==================
// ==================
// CTA Button
// ==================
// Variables
$button-primary-color: lighten(desaturate(blue, 35%), 5%);
$button-secondary-color: lighten(desaturate(red, 35%), 5%);
$button-tertiary-color: lighten(desaturate(green, 45%), 15%);
$button-neutral-color: #333;
$button-disabled-color: #bbb;
$button-border-radius: 3px;
// Mixins
@mixin button-base(
$font-size,
$margin,
$padding,
$loading-padding-right) {
.button {
padding: $padding;
font-size: $font-size;
&:not(:last-of-type) {
margin-right: $margin;
}
&--loading {
padding-right: $loading-padding-right
}
}
}
// Responsive
@media (max-width: $g-breakpoint-xs) {
@include button-base(
.6em,
.15em,
.25em .5em,
1.5em);
}
@media (min-width: $g-breakpoint-xs + 1)
and (max-width: $g-breakpoint-sm) {
@include button-base(
.75em,
.25em,
.25em .5em .15em,
1.5em);
}
@media (min-width: $g-breakpoint-sm + 1)
and (max-width: $g-breakpoint-md) {
@include button-base(
.9em,
.5em,
.5em .75em .4em,
1.5em);
}
@media (min-width: $g-breakpoint-md) {
@include button-base(
1em,
.75em,
.75em 1em .6em,
1.5em);
}
// Component
.button {
border: 1px solid;
font-weight: normal;
border-radius: $button-border-radius;
background: none;
box-shadow: none;
transition: all $g-transition-duration;
&--compound {
border-radius: 0;
border-right: none;
&-first {
border-bottom-left-radius: $button-border-radius;
border-top-left-radius: $button-border-radius;
color: red;
}
&-last {
border-bottom-right-radius: $button-border-radius;
border-top-right-radius: $button-border-radius;
border-right: 1px solid;
color: blue;
}
}
&--primary {
color: $button-primary-color;
font-weight: 600;
&:hover,
&:focus,
&:active {
border-color: $button-primary-color;
background: $button-primary-color;
}
&:focus {
box-shadow: 0 0 5px $button-primary-color;
}
}
&--secondary {
color: $button-secondary-color;
&:hover,
&:focus,
&:active {
border-color: $button-secondary-color;
background: $button-secondary-color;
}
&:focus {
box-shadow: 0 0 5px $button-secondary-color;
}
}
&--tertiary {
color: #fff;
border-color: $button-tertiary-color;
background: $button-tertiary-color;
pointer-events: none;
}
&--neutral {
color: $button-neutral-color;
&:hover,
&:focus,
&:active {
border-color: $button-neutral-color;
background: $button-neutral-color;
}
&:focus {
box-shadow: 0 0 5px $button-neutral-color;
}
}
&--disabled {
border-color: $button-disabled-color;
color: $button-disabled-color;
pointer-events: none;
}
&--loading {
&-text::after {
display: inline-block;
width: 1px;
height: 1px;
content: '';
box-shadow: 2px -2px 1px 0;
animation: progress 1s infinite;
}
}
&:hover,
&:focus,
&:active {
color: #fff;
}
&:focus {
outline: none;
}
&:active {
box-shadow: inset 0 -2px 10px rgba(#000, .4);
}
}
// ==================
// Feedback Messages
// ==================
// Component
.message {
font-size: .9em;
padding: 2em;
margin: 0;
border-radius: 3px;
color: rgba(black, .75);
&--busy {
background: rgba(blue, .1)
}
&--fail {
background: rgba(red, .1)
}
}
// ==================
// Sudoku Table
// ==================
// Variables
$sudoku-color: #444;
// Mixins
@mixin sudoku-base(
$thin-border,
$thick-border,
$cell-size,
$font-size,
$title-size,
$padding-top,
$header-padding
) {
.sudoku {
margin: 0 auto;
padding-top: $padding-top;
&__header {
padding-bottom: $header-padding
}
&__title {
font-size: $title-size
}
&__table {
font-size: $font-size;
border-top: $thick-border;
border-left: $thick-border;
&-row {
border-bottom: $thin-border;
border-right: $thick-border;
&--separator {
border-bottom: $thick-border;
}
}
&-cell {
width: $cell-size;
height: $cell-size;
border-right: $thin-border;
&--separator {
border-right: $thick-border;
}
}
}
}
}
// Responsive
@media (max-width: $g-breakpoint-xs) {
@include sudoku-base(
1px solid $sudoku-color,
2px solid $sudoku-color,
16px, .9em, 1em, .5em, .6em);
.sudoku {
max-width: calc(#{$g-breakpoint-xs} / 1.5);
min-width: calc(#{$g-breakpoint-xs} / 2);
}
}
@media (min-width: $g-breakpoint-xs + 1)
and (max-width: $g-breakpoint-sm) {
@include sudoku-base(
1px solid $sudoku-color,
3px solid $sudoku-color,
32px, 1.2em, 1.2em, 1em, .9em);
.sudoku {
width: $g-breakpoint-xs;
}
}
@media (min-width: $g-breakpoint-sm + 1)
and (max-width: $g-breakpoint-md) {
@include sudoku-base(
1px solid $sudoku-color,
4px solid $sudoku-color,
48px, 1.5em, 1.5em, 2em, 1.3em);
.sudoku {
width: $g-breakpoint-sm
}
}
@media (min-width: $g-breakpoint-md) {
@include sudoku-base(
2px solid $sudoku-color,
6px solid $sudoku-color,
64px, 1.75em, 2em, 3em, 1.618em);
.sudoku {
width: $g-breakpoint-md
}
}
// Component
.sudoku {
color: $sudoku-color;
&__header {
font-family: Gill, sans-serif;
}
&__title {
font-weight: 600
}
&__description {
max-width: 640px;
line-height: 1.4;
font-weight: 100
}
&__table {
background: #fff;
&-row {}
&-cell {
overflow: hidden;
text-align: center;
transition: all .25s;
&--editable {
color: desaturate(blue, 25);
&:focus {
background: rgba(blue, .1);
outline: none;
}
}
&--error {
color: red;
background: #fdd;
}
&--editable-error {
text-shadow: 0 0 15px;
&:focus {
color: #eee;
background: #f45;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment