├── README.md ├── alpine.js ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── atom-one-dark.css ├── atom-one-light.css ├── demo.png ├── examples.html ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── highlight.pack.js ├── index.html ├── logo.png ├── profile.png ├── ridge-dark.css ├── ridge-light.css ├── ridge.css ├── site.webmanifest ├── social.png └── themes ├── ridge-valentines-dark.css ├── ridge-valentines-light.css ├── ridge-watermelon-dark.css └── ridge-watermelon-light.css /README.md: -------------------------------------------------------------------------------- 1 | # ridge.css 2 | 3 | > The clearest way into the Universe is through a forest wilderness. - John Muir 4 | 5 | ridge.css is a set of classless css themes, layout helpers via [pylon.css](https://github.com/almonk/pylon) and optional [alpine.js](https://github.com/alpinejs/alpine) code to help you write _fast web apps fast_ 6 | 7 | ## Usage 8 | Try before you buy! Here's how your html could look after applying ridge.css (with the default dark theme) 9 | 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | ridge.css 17 | 18 | 19 | 20 | 21 | 22 | 23 | Cool logo 24 | 25 | 26 | Pricing 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |

Welcome to ridge.css!

35 |
36 |
37 | 38 | 39 | ``` 40 | 41 | Here's a screenshot of how that looks: 42 | 43 | ![screenshot of default dark theme](demo.png) 44 | 45 | ## Install 46 | 47 | Download a theme, and add it to your html: 48 | 49 | ```html 50 | 51 | 52 | 53 | 54 | 55 | ``` 56 | 57 | The example above adds both light and dark default themes and switches depending on the OS's dark mode setting. 58 | -------------------------------------------------------------------------------- /alpine.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.Alpine = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | function _defineProperty(obj, key, value) { 8 | if (key in obj) { 9 | Object.defineProperty(obj, key, { 10 | value: value, 11 | enumerable: true, 12 | configurable: true, 13 | writable: true 14 | }); 15 | } else { 16 | obj[key] = value; 17 | } 18 | 19 | return obj; 20 | } 21 | 22 | function ownKeys(object, enumerableOnly) { 23 | var keys = Object.keys(object); 24 | 25 | if (Object.getOwnPropertySymbols) { 26 | var symbols = Object.getOwnPropertySymbols(object); 27 | if (enumerableOnly) symbols = symbols.filter(function (sym) { 28 | return Object.getOwnPropertyDescriptor(object, sym).enumerable; 29 | }); 30 | keys.push.apply(keys, symbols); 31 | } 32 | 33 | return keys; 34 | } 35 | 36 | function _objectSpread2(target) { 37 | for (var i = 1; i < arguments.length; i++) { 38 | var source = arguments[i] != null ? arguments[i] : {}; 39 | 40 | if (i % 2) { 41 | ownKeys(Object(source), true).forEach(function (key) { 42 | _defineProperty(target, key, source[key]); 43 | }); 44 | } else if (Object.getOwnPropertyDescriptors) { 45 | Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); 46 | } else { 47 | ownKeys(Object(source)).forEach(function (key) { 48 | Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); 49 | }); 50 | } 51 | } 52 | 53 | return target; 54 | } 55 | 56 | // Thanks @stimulus: 57 | // https://github.com/stimulusjs/stimulus/blob/master/packages/%40stimulus/core/src/application.ts 58 | function domReady() { 59 | return new Promise(resolve => { 60 | if (document.readyState == "loading") { 61 | document.addEventListener("DOMContentLoaded", resolve); 62 | } else { 63 | resolve(); 64 | } 65 | }); 66 | } 67 | function arrayUnique(array) { 68 | return Array.from(new Set(array)); 69 | } 70 | function isTesting() { 71 | return navigator.userAgent.includes("Node.js") || navigator.userAgent.includes("jsdom"); 72 | } 73 | function kebabCase(subject) { 74 | return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase(); 75 | } 76 | function walk(el, callback) { 77 | if (callback(el) === false) return; 78 | let node = el.firstElementChild; 79 | 80 | while (node) { 81 | walk(node, callback); 82 | node = node.nextElementSibling; 83 | } 84 | } 85 | function debounce(func, wait) { 86 | var timeout; 87 | return function () { 88 | var context = this, 89 | args = arguments; 90 | 91 | var later = function later() { 92 | timeout = null; 93 | func.apply(context, args); 94 | }; 95 | 96 | clearTimeout(timeout); 97 | timeout = setTimeout(later, wait); 98 | }; 99 | } 100 | function saferEval(expression, dataContext, additionalHelperVariables = {}) { 101 | return new Function(['$data', ...Object.keys(additionalHelperVariables)], `var result; with($data) { result = ${expression} }; return result`)(dataContext, ...Object.values(additionalHelperVariables)); 102 | } 103 | function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) { 104 | // For the cases when users pass only a function reference to the caller: `x-on:click="foo"` 105 | // Where "foo" is a function. Also, we'll pass the function the event instance when we call it. 106 | if (Object.keys(dataContext).includes(expression)) { 107 | let methodReference = new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { return ${expression} }`)(dataContext, ...Object.values(additionalHelperVariables)); 108 | 109 | if (typeof methodReference === 'function') { 110 | return methodReference.call(dataContext, additionalHelperVariables['$event']); 111 | } 112 | } 113 | 114 | return new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { ${expression} }`)(dataContext, ...Object.values(additionalHelperVariables)); 115 | } 116 | const xAttrRE = /^x-(on|bind|data|text|html|model|if|for|show|cloak|transition|ref)\b/; 117 | function isXAttr(attr) { 118 | const name = replaceAtAndColonWithStandardSyntax(attr.name); 119 | return xAttrRE.test(name); 120 | } 121 | function getXAttrs(el, type) { 122 | return Array.from(el.attributes).filter(isXAttr).map(attr => { 123 | const name = replaceAtAndColonWithStandardSyntax(attr.name); 124 | const typeMatch = name.match(xAttrRE); 125 | const valueMatch = name.match(/:([a-zA-Z\-:]+)/); 126 | const modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []; 127 | return { 128 | type: typeMatch ? typeMatch[1] : null, 129 | value: valueMatch ? valueMatch[1] : null, 130 | modifiers: modifiers.map(i => i.replace('.', '')), 131 | expression: attr.value 132 | }; 133 | }).filter(i => { 134 | // If no type is passed in for filtering, bypass filter 135 | if (!type) return true; 136 | return i.type === type; 137 | }); 138 | } 139 | function isBooleanAttr(attrName) { 140 | // As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute 141 | // Array roughly ordered by estimated usage 142 | const booleanAttributes = ['disabled', 'checked', 'required', 'readonly', 'hidden', 'open', 'selected', 'autofocus', 'itemscope', 'multiple', 'novalidate', 'allowfullscreen', 'allowpaymentrequest', 'formnovalidate', 'autoplay', 'controls', 'loop', 'muted', 'playsinline', 'default', 'ismap', 'reversed', 'async', 'defer', 'nomodule']; 143 | return booleanAttributes.includes(attrName); 144 | } 145 | function replaceAtAndColonWithStandardSyntax(name) { 146 | if (name.startsWith('@')) { 147 | return name.replace('@', 'x-on:'); 148 | } else if (name.startsWith(':')) { 149 | return name.replace(':', 'x-bind:'); 150 | } 151 | 152 | return name; 153 | } 154 | function transitionIn(el, show, forceSkip = false) { 155 | // We don't want to transition on the initial page load. 156 | if (forceSkip) return show(); 157 | const attrs = getXAttrs(el, 'transition'); 158 | const showAttr = getXAttrs(el, 'show')[0]; // If this is triggered by a x-show.transition. 159 | 160 | if (showAttr && showAttr.modifiers.includes('transition')) { 161 | let modifiers = showAttr.modifiers; // If x-show.transition.out, we'll skip the "in" transition. 162 | 163 | if (modifiers.includes('out') && !modifiers.includes('in')) return show(); 164 | const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out'); // If x-show.transition.in...out... only use "in" related modifiers for this transition. 165 | 166 | modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers; 167 | transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter. 168 | } else if (attrs.length > 0) { 169 | transitionClassesIn(el, attrs, show); 170 | } else { 171 | // If neither, just show that damn thing. 172 | show(); 173 | } 174 | } 175 | function transitionOut(el, hide, forceSkip = false) { 176 | if (forceSkip) return hide(); 177 | const attrs = getXAttrs(el, 'transition'); 178 | const showAttr = getXAttrs(el, 'show')[0]; 179 | 180 | if (showAttr && showAttr.modifiers.includes('transition')) { 181 | let modifiers = showAttr.modifiers; 182 | if (modifiers.includes('in') && !modifiers.includes('out')) return hide(); 183 | const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out'); 184 | modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers; 185 | transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide); 186 | } else if (attrs.length > 0) { 187 | transitionClassesOut(el, attrs, hide); 188 | } else { 189 | hide(); 190 | } 191 | } 192 | function transitionHelperIn(el, modifiers, showCallback) { 193 | // Default values inspired by: https://material.io/design/motion/speed.html#duration 194 | const styleValues = { 195 | duration: modifierValue(modifiers, 'duration', 150), 196 | origin: modifierValue(modifiers, 'origin', 'center'), 197 | first: { 198 | opacity: 0, 199 | scale: modifierValue(modifiers, 'scale', 95) 200 | }, 201 | second: { 202 | opacity: 1, 203 | scale: 100 204 | } 205 | }; 206 | transitionHelper(el, modifiers, showCallback, () => {}, styleValues); 207 | } 208 | function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) { 209 | // Make the "out" transition .5x slower than the "in". (Visually better) 210 | // HOWEVER, if they explicitly set a duration for the "out" transition, 211 | // use that. 212 | const duration = settingBothSidesOfTransition ? modifierValue(modifiers, 'duration', 150) : modifierValue(modifiers, 'duration', 150) / 2; 213 | const styleValues = { 214 | duration: duration, 215 | origin: modifierValue(modifiers, 'origin', 'center'), 216 | first: { 217 | opacity: 1, 218 | scale: 100 219 | }, 220 | second: { 221 | opacity: 0, 222 | scale: modifierValue(modifiers, 'scale', 95) 223 | } 224 | }; 225 | transitionHelper(el, modifiers, () => {}, hideCallback, styleValues); 226 | } 227 | 228 | function modifierValue(modifiers, key, fallback) { 229 | // If the modifier isn't present, use the default. 230 | if (modifiers.indexOf(key) === -1) return fallback; // If it IS present, grab the value after it: x-show.transition.duration.500ms 231 | 232 | const rawValue = modifiers[modifiers.indexOf(key) + 1]; 233 | if (!rawValue) return fallback; 234 | 235 | if (key === 'scale') { 236 | // Check if the very next value is NOT a number and return the fallback. 237 | // If x-show.transition.scale, we'll use the default scale value. 238 | // That is how a user opts out of the opacity transition. 239 | if (!isNumeric(rawValue)) return fallback; 240 | } 241 | 242 | if (key === 'duration') { 243 | // Support x-show.transition.duration.500ms && duration.500 244 | let match = rawValue.match(/([0-9]+)ms/); 245 | if (match) return match[1]; 246 | } 247 | 248 | if (key === 'origin') { 249 | // Support chaining origin directions: x-show.transition.top.right 250 | if (['top', 'right', 'left', 'center', 'bottom'].includes(modifiers[modifiers.indexOf(key) + 2])) { 251 | return [rawValue, modifiers[modifiers.indexOf(key) + 2]].join(' '); 252 | } 253 | } 254 | 255 | return rawValue; 256 | } 257 | 258 | function transitionHelper(el, modifiers, hook1, hook2, styleValues) { 259 | // If the user set these style values, we'll put them back when we're done with them. 260 | const opacityCache = el.style.opacity; 261 | const transformCache = el.style.transform; 262 | const transformOriginCache = el.style.transformOrigin; // If no modifiers are present: x-show.transition, we'll default to both opacity and scale. 263 | 264 | const noModifiers = !modifiers.includes('opacity') && !modifiers.includes('scale'); 265 | const transitionOpacity = noModifiers || modifiers.includes('opacity'); 266 | const transitionScale = noModifiers || modifiers.includes('scale'); // These are the explicit stages of a transition (same stages for in and for out). 267 | // This way you can get a birds eye view of the hooks, and the differences 268 | // between them. 269 | 270 | const stages = { 271 | start() { 272 | if (transitionOpacity) el.style.opacity = styleValues.first.opacity; 273 | if (transitionScale) el.style.transform = `scale(${styleValues.first.scale / 100})`; 274 | }, 275 | 276 | during() { 277 | if (transitionScale) el.style.transformOrigin = styleValues.origin; 278 | el.style.transitionProperty = [transitionOpacity ? `opacity` : ``, transitionScale ? `transform` : ``].join(' ').trim(); 279 | el.style.transitionDuration = `${styleValues.duration / 1000}s`; 280 | el.style.transitionTimingFunction = `cubic-bezier(0.4, 0.0, 0.2, 1)`; 281 | }, 282 | 283 | show() { 284 | hook1(); 285 | }, 286 | 287 | end() { 288 | if (transitionOpacity) el.style.opacity = styleValues.second.opacity; 289 | if (transitionScale) el.style.transform = `scale(${styleValues.second.scale / 100})`; 290 | }, 291 | 292 | hide() { 293 | hook2(); 294 | }, 295 | 296 | cleanup() { 297 | if (transitionOpacity) el.style.opacity = opacityCache; 298 | if (transitionScale) el.style.transform = transformCache; 299 | if (transitionScale) el.style.transformOrigin = transformOriginCache; 300 | el.style.transitionProperty = null; 301 | el.style.transitionDuration = null; 302 | el.style.transitionTimingFunction = null; 303 | } 304 | 305 | }; 306 | transition(el, stages); 307 | } 308 | function transitionClassesIn(el, directives, showCallback) { 309 | const enter = (directives.find(i => i.value === 'enter') || { 310 | expression: '' 311 | }).expression.split(' ').filter(i => i !== ''); 312 | const enterStart = (directives.find(i => i.value === 'enter-start') || { 313 | expression: '' 314 | }).expression.split(' ').filter(i => i !== ''); 315 | const enterEnd = (directives.find(i => i.value === 'enter-end') || { 316 | expression: '' 317 | }).expression.split(' ').filter(i => i !== ''); 318 | transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}); 319 | } 320 | function transitionClassesOut(el, directives, hideCallback) { 321 | const leave = (directives.find(i => i.value === 'leave') || { 322 | expression: '' 323 | }).expression.split(' ').filter(i => i !== ''); 324 | const leaveStart = (directives.find(i => i.value === 'leave-start') || { 325 | expression: '' 326 | }).expression.split(' ').filter(i => i !== ''); 327 | const leaveEnd = (directives.find(i => i.value === 'leave-end') || { 328 | expression: '' 329 | }).expression.split(' ').filter(i => i !== ''); 330 | transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback); 331 | } 332 | function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2) { 333 | const originalClasses = el.__x_original_classes || []; 334 | const stages = { 335 | start() { 336 | el.classList.add(...classesStart); 337 | }, 338 | 339 | during() { 340 | el.classList.add(...classesDuring); 341 | }, 342 | 343 | show() { 344 | hook1(); 345 | }, 346 | 347 | end() { 348 | // Don't remove classes that were in the original class attribute. 349 | el.classList.remove(...classesStart.filter(i => !originalClasses.includes(i))); 350 | el.classList.add(...classesEnd); 351 | }, 352 | 353 | hide() { 354 | hook2(); 355 | }, 356 | 357 | cleanup() { 358 | el.classList.remove(...classesDuring.filter(i => !originalClasses.includes(i))); 359 | el.classList.remove(...classesEnd.filter(i => !originalClasses.includes(i))); 360 | } 361 | 362 | }; 363 | transition(el, stages); 364 | } 365 | function transition(el, stages) { 366 | stages.start(); 367 | stages.during(); 368 | requestAnimationFrame(() => { 369 | // Note: Safari's transitionDuration property will list out comma separated transition durations 370 | // for every single transition property. Let's grab the first one and call it a day. 371 | let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000; 372 | stages.show(); 373 | requestAnimationFrame(() => { 374 | stages.end(); 375 | setTimeout(() => { 376 | stages.hide(); // Adding an "isConnected" check, in case the callback 377 | // removed the element from the DOM. 378 | 379 | if (el.isConnected) { 380 | stages.cleanup(); 381 | } 382 | }, duration); 383 | }); 384 | }); 385 | } 386 | function isNumeric(subject) { 387 | return !isNaN(subject); 388 | } 389 | 390 | function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) { 391 | warnIfNotTemplateTag(templateEl); 392 | let iteratorNames = parseForExpression(expression); 393 | let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars); // As we walk the array, we'll also walk the DOM (updating/creating as we go). 394 | 395 | let currentEl = templateEl; 396 | items.forEach((item, index) => { 397 | let iterationScopeVariables = getIterationScopeVariables(iteratorNames, item, index, items, extraVars()); 398 | let currentKey = generateKeyForIteration(component, templateEl, index, iterationScopeVariables); 399 | let nextEl = lookAheadForMatchingKeyedElementAndMoveItIfFound(currentEl.nextElementSibling, currentKey); // If we haven't found a matching key, insert the element at the current position. 400 | 401 | if (!nextEl) { 402 | nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl); // And transition it in if it's not the first page load. 403 | 404 | transitionIn(nextEl, () => {}, initialUpdate); 405 | nextEl.__x_for = iterationScopeVariables; 406 | component.initializeElements(nextEl, () => nextEl.__x_for); // Otherwise update the element we found. 407 | } else { 408 | // Temporarily remove the key indicator to allow the normal "updateElements" to work. 409 | delete nextEl.__x_for_key; 410 | nextEl.__x_for = iterationScopeVariables; 411 | component.updateElements(nextEl, () => nextEl.__x_for); 412 | } 413 | 414 | currentEl = nextEl; 415 | currentEl.__x_for_key = currentKey; 416 | }); 417 | removeAnyLeftOverElementsFromPreviousUpdate(currentEl); 418 | } // This was taken from VueJS 2.* core. Thanks Vue! 419 | 420 | function parseForExpression(expression) { 421 | let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/; 422 | let stripParensRE = /^\(|\)$/g; 423 | let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/; 424 | let inMatch = expression.match(forAliasRE); 425 | if (!inMatch) return; 426 | let res = {}; 427 | res.items = inMatch[2].trim(); 428 | let item = inMatch[1].trim().replace(stripParensRE, ''); 429 | let iteratorMatch = item.match(forIteratorRE); 430 | 431 | if (iteratorMatch) { 432 | res.item = item.replace(forIteratorRE, '').trim(); 433 | res.index = iteratorMatch[1].trim(); 434 | 435 | if (iteratorMatch[2]) { 436 | res.collection = iteratorMatch[2].trim(); 437 | } 438 | } else { 439 | res.item = item; 440 | } 441 | 442 | return res; 443 | } 444 | 445 | function getIterationScopeVariables(iteratorNames, item, index, items, extraVars) { 446 | // We must create a new object, so each iteration has a new scope 447 | let scopeVariables = extraVars ? _objectSpread2({}, extraVars) : {}; 448 | scopeVariables[iteratorNames.item] = item; 449 | if (iteratorNames.index) scopeVariables[iteratorNames.index] = index; 450 | if (iteratorNames.collection) scopeVariables[iteratorNames.collection] = items; 451 | return scopeVariables; 452 | } 453 | 454 | function generateKeyForIteration(component, el, index, iterationScopeVariables) { 455 | let bindKeyAttribute = getXAttrs(el, 'bind').filter(attr => attr.value === 'key')[0]; // If the dev hasn't specified a key, just return the index of the iteration. 456 | 457 | if (!bindKeyAttribute) return index; 458 | return component.evaluateReturnExpression(el, bindKeyAttribute.expression, () => iterationScopeVariables); 459 | } 460 | 461 | function warnIfNotTemplateTag(el) { 462 | if (el.tagName.toLowerCase() !== 'template') console.warn('Alpine: [x-for] directive should only be added to