mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-21 18:48:36 +00:00
build
This commit is contained in:
parent
240ebede6f
commit
22baae10dd
@ -2,7 +2,7 @@ import Fraction from "../pkg/fractionjs.js";
|
|||||||
const removeUndefineds = (xs) => xs.filter((x) => x != void 0);
|
const removeUndefineds = (xs) => xs.filter((x) => x != void 0);
|
||||||
const flatten = (arr) => [].concat(...arr);
|
const flatten = (arr) => [].concat(...arr);
|
||||||
const id = (a) => a;
|
const id = (a) => a;
|
||||||
function curry(func) {
|
export function curry(func) {
|
||||||
return function curried(...args) {
|
return function curried(...args) {
|
||||||
if (args.length >= func.length) {
|
if (args.length >= func.length) {
|
||||||
return func.apply(this, args);
|
return func.apply(this, args);
|
||||||
@ -134,6 +134,14 @@ class Hap {
|
|||||||
class Pattern {
|
class Pattern {
|
||||||
constructor(query) {
|
constructor(query) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
|
const proto = Object.getPrototypeOf(this);
|
||||||
|
proto.patternified.forEach((prop) => {
|
||||||
|
this[prop] = (...args) => this._patternify(Pattern.prototype["_" + prop])(...args);
|
||||||
|
Object.assign(this[prop], Object.fromEntries(Object.entries(Pattern.prototype.factories).map(([type, func]) => [
|
||||||
|
type,
|
||||||
|
(...args) => this[prop](func(...args))
|
||||||
|
])));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_splitQueries() {
|
_splitQueries() {
|
||||||
const pat = this;
|
const pat = this;
|
||||||
@ -321,28 +329,16 @@ class Pattern {
|
|||||||
const fastQuery = this.withQueryTime((t) => t.mul(factor));
|
const fastQuery = this.withQueryTime((t) => t.mul(factor));
|
||||||
return fastQuery.withEventTime((t) => t.div(factor));
|
return fastQuery.withEventTime((t) => t.div(factor));
|
||||||
}
|
}
|
||||||
fast(...factor) {
|
|
||||||
return this._patternify(Pattern.prototype._fast)(...factor);
|
|
||||||
}
|
|
||||||
_slow(factor) {
|
_slow(factor) {
|
||||||
return this._fast(1 / factor);
|
return this._fast(1 / factor);
|
||||||
}
|
}
|
||||||
slow(...factor) {
|
|
||||||
return this._patternify(Pattern.prototype._slow)(...factor);
|
|
||||||
}
|
|
||||||
_early(offset) {
|
_early(offset) {
|
||||||
offset = Fraction(offset);
|
offset = Fraction(offset);
|
||||||
return this.withQueryTime((t) => t.add(offset)).withEventTime((t) => t.sub(offset));
|
return this.withQueryTime((t) => t.add(offset)).withEventTime((t) => t.sub(offset));
|
||||||
}
|
}
|
||||||
early(...factor) {
|
|
||||||
return this._patternify(Pattern.prototype._early)(...factor);
|
|
||||||
}
|
|
||||||
_late(offset) {
|
_late(offset) {
|
||||||
return this._early(0 - offset);
|
return this._early(0 - offset);
|
||||||
}
|
}
|
||||||
late(...factor) {
|
|
||||||
return this._patternify(Pattern.prototype._late)(...factor);
|
|
||||||
}
|
|
||||||
when(binary_pat, func) {
|
when(binary_pat, func) {
|
||||||
const true_pat = binary_pat._filterValues(id);
|
const true_pat = binary_pat._filterValues(id);
|
||||||
const false_pat = binary_pat._filterValues((val) => !val);
|
const false_pat = binary_pat._filterValues((val) => !val);
|
||||||
@ -391,7 +387,28 @@ class Pattern {
|
|||||||
const right = this.withValue((val) => Object.assign({}, val, {pan: elem_or(val, "pan", 0.5) + by}));
|
const right = this.withValue((val) => Object.assign({}, val, {pan: elem_or(val, "pan", 0.5) + by}));
|
||||||
return stack([left, func(right)]);
|
return stack([left, func(right)]);
|
||||||
}
|
}
|
||||||
|
stack(...pats) {
|
||||||
|
return stack(this, ...pats);
|
||||||
|
}
|
||||||
|
sequence(...pats) {
|
||||||
|
return sequence(this, ...pats);
|
||||||
|
}
|
||||||
|
superimpose(...funcs) {
|
||||||
|
return this.stack(...funcs.map((func) => func(this)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Pattern.prototype.patternified = ["fast", "slow", "early", "late"];
|
||||||
|
Pattern.prototype.factories = {pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr};
|
||||||
|
const hackStrings = () => {
|
||||||
|
const pureGetter = {
|
||||||
|
get: function() {
|
||||||
|
return pure(String(this));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.defineProperty(String.prototype, "pure", pureGetter);
|
||||||
|
Object.defineProperty(String.prototype, "p", pureGetter);
|
||||||
|
};
|
||||||
|
hackStrings();
|
||||||
const silence = new Pattern((_) => []);
|
const silence = new Pattern((_) => []);
|
||||||
function pure(value) {
|
function pure(value) {
|
||||||
function query(span) {
|
function query(span) {
|
||||||
@ -418,6 +435,9 @@ function slowcat(...pats) {
|
|||||||
const query = function(span) {
|
const query = function(span) {
|
||||||
const pat_n = Math.floor(span.begin) % pats.length;
|
const pat_n = Math.floor(span.begin) % pats.length;
|
||||||
const pat = pats[pat_n];
|
const pat = pats[pat_n];
|
||||||
|
if (!pat) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
|
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
|
||||||
return pat.withEventTime((t) => t.add(offset)).query(span.withTime((t) => t.sub(offset)));
|
return pat.withEventTime((t) => t.add(offset)).query(span.withTime((t) => t.sub(offset)));
|
||||||
};
|
};
|
||||||
|
|||||||
12
docs/dist/App.js
vendored
12
docs/dist/App.js
vendored
@ -77,8 +77,14 @@ function App() {
|
|||||||
});
|
});
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const handleKeyPress = (e) => {
|
const handleKeyPress = (e) => {
|
||||||
if (e.ctrlKey && e.code === "Enter") {
|
if (e.ctrlKey || e.altKey) {
|
||||||
activatePattern();
|
switch (e.code) {
|
||||||
|
case "Enter":
|
||||||
|
activatePattern();
|
||||||
|
break;
|
||||||
|
case "Period":
|
||||||
|
cycle.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("keypress", handleKeyPress);
|
document.addEventListener("keypress", handleKeyPress);
|
||||||
@ -164,7 +170,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), /* @__PURE__ */ React.createElement("span", {
|
}), /* @__PURE__ */ React.createElement("span", {
|
||||||
className: "p-4 absolute bottom-0 right-0 text-xs whitespace-pre text-right"
|
className: "p-4 absolute top-0 right-0 text-xs whitespace-pre text-right"
|
||||||
}, !cycle.started ? `press ctrl+enter to play
|
}, !cycle.started ? `press ctrl+enter to play
|
||||||
` : !isHot && activePattern !== pattern ? `ctrl+enter to update
|
` : !isHot && activePattern !== pattern ? `ctrl+enter to update
|
||||||
` : "no changes\n", !isHot && /* @__PURE__ */ React.createElement(React.Fragment, null, {pegjs: "mini"}[mode] || mode, " mode"), isHot && "🔥 hot mode: go to hot.js to edit pattern, then save")), error && /* @__PURE__ */ React.createElement("div", {
|
` : "no changes\n", !isHot && /* @__PURE__ */ React.createElement(React.Fragment, null, {pegjs: "mini"}[mode] || mode, " mode"), isHot && "🔥 hot mode: go to hot.js to edit pattern, then save")), error && /* @__PURE__ */ React.createElement("div", {
|
||||||
|
|||||||
1
docs/dist/midi.js
vendored
1
docs/dist/midi.js
vendored
@ -24,6 +24,7 @@ Pattern.prototype.midi = function(output, channel = 1) {
|
|||||||
return this.fmap((value) => ({
|
return this.fmap((value) => ({
|
||||||
...value,
|
...value,
|
||||||
onTrigger: (time, event) => {
|
onTrigger: (time, event) => {
|
||||||
|
value = value.value || value;
|
||||||
if (!isNote(value)) {
|
if (!isNote(value)) {
|
||||||
throw new Error("not a note: " + value);
|
throw new Error("not a note: " + value);
|
||||||
}
|
}
|
||||||
|
|||||||
56
docs/dist/parse.js
vendored
56
docs/dist/parse.js
vendored
@ -5,28 +5,45 @@ import "./tone.js";
|
|||||||
import "./midi.js";
|
import "./midi.js";
|
||||||
import "./voicings.js";
|
import "./voicings.js";
|
||||||
import "./tonal.js";
|
import "./tonal.js";
|
||||||
|
import * as tonalStuff from "./tonal.js";
|
||||||
import "./groove.js";
|
import "./groove.js";
|
||||||
import * as toneStuff from "./tone.js";
|
import * as toneStuff from "./tone.js";
|
||||||
import shapeshifter from "./shapeshifter.js";
|
import shapeshifter from "./shapeshifter.js";
|
||||||
const {
|
const {
|
||||||
|
Fraction,
|
||||||
|
TimeSpan,
|
||||||
|
Hap,
|
||||||
|
Pattern,
|
||||||
pure,
|
pure,
|
||||||
stack,
|
stack,
|
||||||
slowcat,
|
slowcat,
|
||||||
fastcat,
|
fastcat,
|
||||||
cat,
|
cat,
|
||||||
|
timeCat,
|
||||||
sequence,
|
sequence,
|
||||||
polymeter,
|
polymeter,
|
||||||
pm,
|
pm,
|
||||||
polyrhythm,
|
polyrhythm,
|
||||||
pr,
|
pr,
|
||||||
silence,
|
silence,
|
||||||
timeCat,
|
fast,
|
||||||
Fraction,
|
slow,
|
||||||
Pattern,
|
early,
|
||||||
TimeSpan,
|
late,
|
||||||
Hap
|
rev,
|
||||||
|
add,
|
||||||
|
sub,
|
||||||
|
mul,
|
||||||
|
div,
|
||||||
|
union,
|
||||||
|
every,
|
||||||
|
when,
|
||||||
|
off,
|
||||||
|
jux,
|
||||||
|
append
|
||||||
} = strudel;
|
} = strudel;
|
||||||
const {autofilter, filter, gain} = toneStuff;
|
const {autofilter, filter, gain} = toneStuff;
|
||||||
|
const {transpose} = tonalStuff;
|
||||||
function reify(thing) {
|
function reify(thing) {
|
||||||
if (thing?.constructor?.name === "Pattern") {
|
if (thing?.constructor?.name === "Pattern") {
|
||||||
return thing;
|
return thing;
|
||||||
@ -113,10 +130,16 @@ export const mini = (...strings) => {
|
|||||||
return pattern;
|
return pattern;
|
||||||
};
|
};
|
||||||
const m = mini;
|
const m = mini;
|
||||||
const s = (...strings) => {
|
const hackStrings = () => {
|
||||||
const patternified = strings.map((s2) => minify(s2));
|
const miniGetter = {
|
||||||
return stack(...patternified);
|
get: function() {
|
||||||
|
return mini(String(this));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.defineProperty(String.prototype, "mini", miniGetter);
|
||||||
|
Object.defineProperty(String.prototype, "m", miniGetter);
|
||||||
};
|
};
|
||||||
|
hackStrings();
|
||||||
export const h = (string) => {
|
export const h = (string) => {
|
||||||
const ast = krill.parse(string);
|
const ast = krill.parse(string);
|
||||||
return patternifyAST(ast);
|
return patternifyAST(ast);
|
||||||
@ -124,17 +147,12 @@ export const h = (string) => {
|
|||||||
export const parse = (code) => {
|
export const parse = (code) => {
|
||||||
let _pattern;
|
let _pattern;
|
||||||
let mode;
|
let mode;
|
||||||
try {
|
mode = "javascript";
|
||||||
_pattern = h(code);
|
code = shapeshifter(code);
|
||||||
mode = "pegjs";
|
_pattern = minify(eval(code));
|
||||||
} catch (err) {
|
if (_pattern?.constructor?.name !== "Pattern") {
|
||||||
mode = "javascript";
|
const message = `got "${typeof _pattern}" instead of pattern`;
|
||||||
code = shapeshifter(code);
|
throw new Error(message + (typeof _pattern === "function" ? ", did you forget to call a function?" : "."));
|
||||||
_pattern = eval(code);
|
|
||||||
if (_pattern?.constructor?.name !== "Pattern") {
|
|
||||||
const message = `got "${typeof _pattern}" instead of pattern`;
|
|
||||||
throw new Error(message + (typeof _pattern === "function" ? ", did you forget to call a function?" : "."));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {mode, pattern: _pattern};
|
return {mode, pattern: _pattern};
|
||||||
};
|
};
|
||||||
|
|||||||
13
docs/dist/tonal.js
vendored
13
docs/dist/tonal.js
vendored
@ -1,5 +1,5 @@
|
|||||||
import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
|
import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
|
||||||
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
|
import {Pattern as _Pattern, curry} from "../_snowpack/link/strudel.js";
|
||||||
const Pattern = _Pattern;
|
const Pattern = _Pattern;
|
||||||
function toNoteEvent(event) {
|
function toNoteEvent(event) {
|
||||||
if (typeof event === "string") {
|
if (typeof event === "string") {
|
||||||
@ -55,9 +55,7 @@ Pattern.prototype._transpose = function(intervalOrSemitones) {
|
|||||||
return {value: Note.transpose(value, interval), scale};
|
return {value: Note.transpose(value, interval), scale};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Pattern.prototype.transpose = function(intervalOrSemitones) {
|
export const transpose = curry((a, pat) => pat.transpose(a));
|
||||||
return this._patternify(Pattern.prototype._transpose)(intervalOrSemitones);
|
|
||||||
};
|
|
||||||
Pattern.prototype._scaleTranspose = function(offset) {
|
Pattern.prototype._scaleTranspose = function(offset) {
|
||||||
return this._mapNotes(({value, scale}) => {
|
return this._mapNotes(({value, scale}) => {
|
||||||
if (!scale) {
|
if (!scale) {
|
||||||
@ -66,12 +64,7 @@ Pattern.prototype._scaleTranspose = function(offset) {
|
|||||||
return {value: scaleTranspose(scale, Number(offset), value), scale};
|
return {value: scaleTranspose(scale, Number(offset), value), scale};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Pattern.prototype.scaleTranspose = function(offset) {
|
|
||||||
return this._patternify(Pattern.prototype._scaleTranspose)(offset);
|
|
||||||
};
|
|
||||||
Pattern.prototype._scale = function(scale) {
|
Pattern.prototype._scale = function(scale) {
|
||||||
return this._mapNotes((value) => ({...value, scale}));
|
return this._mapNotes((value) => ({...value, scale}));
|
||||||
};
|
};
|
||||||
Pattern.prototype.scale = function(scale) {
|
Pattern.prototype.patternified = Pattern.prototype.patternified.concat(["transpose", "scaleTranspose", "scale"]);
|
||||||
return this._patternify(Pattern.prototype._scale)(scale);
|
|
||||||
};
|
|
||||||
|
|||||||
10
docs/dist/tone.js
vendored
10
docs/dist/tone.js
vendored
@ -27,9 +27,6 @@ Pattern.prototype._synth = function(type = "triangle") {
|
|||||||
return {...value, getInstrument, instrumentConfig, onTrigger};
|
return {...value, getInstrument, instrumentConfig, onTrigger};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Pattern.prototype.synth = function(type = "triangle") {
|
|
||||||
return this._patternify(Pattern.prototype._synth)(type);
|
|
||||||
};
|
|
||||||
Pattern.prototype.adsr = function(attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
|
Pattern.prototype.adsr = function(attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
|
||||||
return this.fmap((value) => {
|
return this.fmap((value) => {
|
||||||
if (!value?.getInstrument) {
|
if (!value?.getInstrument) {
|
||||||
@ -65,15 +62,10 @@ export const gain = (gain2 = 0.9) => () => new Gain(gain2);
|
|||||||
Pattern.prototype._gain = function(g) {
|
Pattern.prototype._gain = function(g) {
|
||||||
return this.chain(gain(g));
|
return this.chain(gain(g));
|
||||||
};
|
};
|
||||||
Pattern.prototype.gain = function(g) {
|
|
||||||
return this._patternify(Pattern.prototype._gain)(g);
|
|
||||||
};
|
|
||||||
Pattern.prototype._filter = function(freq, q, type = "lowpass") {
|
Pattern.prototype._filter = function(freq, q, type = "lowpass") {
|
||||||
return this.chain(filter(freq, q, type));
|
return this.chain(filter(freq, q, type));
|
||||||
};
|
};
|
||||||
Pattern.prototype.filter = function(freq) {
|
|
||||||
return this._patternify(Pattern.prototype._filter)(freq);
|
|
||||||
};
|
|
||||||
Pattern.prototype.autofilter = function(g) {
|
Pattern.prototype.autofilter = function(g) {
|
||||||
return this.chain(autofilter(g));
|
return this.chain(autofilter(g));
|
||||||
};
|
};
|
||||||
|
Pattern.prototype.patternified = Pattern.prototype.patternified.concat(["synth", "gain", "filter"]);
|
||||||
|
|||||||
58
docs/dist/tunes.js
vendored
58
docs/dist/tunes.js
vendored
@ -1,7 +1,7 @@
|
|||||||
export const timeCatMini = `s(
|
export const timeCatMini = `stack(
|
||||||
'c3@3 [eb3, g3, [c4 d4]/2]',
|
'c3@3 [eb3, g3, [c4 d4]/2]'.mini,
|
||||||
'c2 g2',
|
'c2 g2'.mini,
|
||||||
m('[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2').slow(8)
|
'[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8)
|
||||||
)`;
|
)`;
|
||||||
export const timeCat = `stack(
|
export const timeCat = `stack(
|
||||||
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]),
|
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]),
|
||||||
@ -260,6 +260,12 @@ export const transposedChords = `stack(
|
|||||||
).transpose(
|
).transpose(
|
||||||
slowcat(1, 2, 3, 2).slow(2)
|
slowcat(1, 2, 3, 2).slow(2)
|
||||||
).transpose(5)`;
|
).transpose(5)`;
|
||||||
|
export const transposedChordsHacked = `stack(
|
||||||
|
'c2 eb2 g2'.mini,
|
||||||
|
'Cm7'.pure.voicings(['g2','c4']).slow(2)
|
||||||
|
).transpose(
|
||||||
|
slowcat(1, 2, 3, 2).slow(2)
|
||||||
|
).transpose(5)`;
|
||||||
export const scaleTranspose = `stack(f2, f3, c4, ab4)
|
export const scaleTranspose = `stack(f2, f3, c4, ab4)
|
||||||
.scale(sequence('F minor', 'F harmonic minor').slow(4))
|
.scale(sequence('F minor', 'F harmonic minor').slow(4))
|
||||||
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
|
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
|
||||||
@ -280,4 +286,48 @@ export const groove = `stack(
|
|||||||
.adsr(.1,.1,.2)
|
.adsr(.1,.1,.2)
|
||||||
.gain(0.25)
|
.gain(0.25)
|
||||||
).slow(4.5)`;
|
).slow(4.5)`;
|
||||||
|
export const grooveHacked = `stack(
|
||||||
|
'c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'.mini
|
||||||
|
.synth('sawtooth')
|
||||||
|
.filter(500)
|
||||||
|
.gain(.6),
|
||||||
|
'[C^7 A7] [Dm7 G7]'.mini.groove('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2'.mini)
|
||||||
|
.voicings(['G3','A4'])
|
||||||
|
.synth('square')
|
||||||
|
.filter(1000)
|
||||||
|
.adsr(.1,.1,.2)
|
||||||
|
.gain(0.25)
|
||||||
|
).slow(4.5)`;
|
||||||
|
export const magicSofa = `stack(
|
||||||
|
m('[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4')
|
||||||
|
.every(2, fast(2))
|
||||||
|
.voicings(),
|
||||||
|
m('[c2 f2 g2]/3 [d2 g2 a2 e2]/4')
|
||||||
|
).slow(1)
|
||||||
|
.transpose.slowcat(0, 2, 3, 4).midi()`;
|
||||||
|
export const magicSofaHacked = `stack(
|
||||||
|
'[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4'.mini
|
||||||
|
.every(2, fast(2))
|
||||||
|
.voicings(),
|
||||||
|
'[c2 f2 g2]/3 [d2 g2 a2 e2]/4'.mini
|
||||||
|
).slow(1)
|
||||||
|
.transpose.slowcat(0, 2, 3, 4).midi()`;
|
||||||
|
export const confusedPhone = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2))
|
||||||
|
.superimpose(
|
||||||
|
x => transpose(-12,x).late(0),
|
||||||
|
x => transpose(7,x).late(0.2),
|
||||||
|
x => transpose(10,x).late(0.4),
|
||||||
|
x => transpose(12,x).late(0.6),
|
||||||
|
x => transpose(24,x).late(0.8)
|
||||||
|
)
|
||||||
|
.scale(sequence('C dorian', 'C mixolydian').slow(4))
|
||||||
|
.scaleTranspose(slowcat(0,1,2,1).slow(2))
|
||||||
|
.synth('triangle').gain(0.2).filter(1500)`;
|
||||||
|
export const confusedPhoneDynamic = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2))
|
||||||
|
.superimpose(
|
||||||
|
...[-12,7,10,12,24].slice(0,5).map((t,i,{length}) => x => transpose(t,x).late(i/length))
|
||||||
|
)
|
||||||
|
.scale(sequence('C dorian', 'C mixolydian').slow(4))
|
||||||
|
.scaleTranspose(slowcat(0,1,2,1).slow(2))
|
||||||
|
.synth('triangle').gain(0.2).filter(1500)`;
|
||||||
export default swimming;
|
export default swimming;
|
||||||
|
|||||||
@ -603,8 +603,8 @@ select {
|
|||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
}
|
}
|
||||||
.bottom-0 {
|
.top-0 {
|
||||||
bottom: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
.right-0 {
|
.right-0 {
|
||||||
right: 0px;
|
right: 0px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user