Merge branch 'master' of https://github.com/naturalcrit/homebrewery into refactor-tag-system
@@ -12,6 +12,7 @@ const CodeEditor = createReactClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
language : '',
|
||||
tab : 'brewText',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
@@ -186,6 +187,22 @@ const CodeEditor = createReactClass({
|
||||
this.updateSize();
|
||||
},
|
||||
|
||||
// Use for GFM tabs that use common hot-keys
|
||||
isGFM : function() {
|
||||
if((this.isGFM()) || (this.props.tab === 'brewSnippets')) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
isBrewText : function() {
|
||||
if(this.isGFM()) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
isBrewSnippets : function() {
|
||||
if(this.props.tab === 'brewSnippets') return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
indent : function () {
|
||||
const cm = this.codeMirror;
|
||||
if(cm.somethingSelected()) {
|
||||
@@ -200,6 +217,7 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeHeader : function (number) {
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror?.getSelection();
|
||||
const header = Array(number).fill('#').join('');
|
||||
this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around');
|
||||
@@ -208,6 +226,7 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeBold : function() {
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
|
||||
if(selection.length === 0){
|
||||
@@ -217,7 +236,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeItalic : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
@@ -226,7 +246,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeSuper : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
@@ -235,7 +256,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeSub : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
@@ -245,10 +267,12 @@ const CodeEditor = createReactClass({
|
||||
|
||||
|
||||
makeNbsp : function() {
|
||||
if(!this.isGFM()) return;
|
||||
this.codeMirror?.replaceSelection(' ', 'end');
|
||||
},
|
||||
|
||||
makeSpace : function() {
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror?.getSelection();
|
||||
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
|
||||
if(t){
|
||||
@@ -260,6 +284,7 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
removeSpace : function() {
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror?.getSelection();
|
||||
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
|
||||
if(t){
|
||||
@@ -269,10 +294,12 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
newColumn : function() {
|
||||
if(!this.isGFM()) return;
|
||||
this.codeMirror?.replaceSelection('\n\\column\n\n', 'end');
|
||||
},
|
||||
|
||||
newPage : function() {
|
||||
if(!this.isGFM()) return;
|
||||
this.codeMirror?.replaceSelection('\n\\page\n\n', 'end');
|
||||
},
|
||||
|
||||
@@ -286,7 +313,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeUnderline : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
@@ -295,7 +323,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeSpan : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
@@ -304,7 +333,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeDiv : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||
if(!this.isGFM()) return;
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
@@ -317,7 +347,7 @@ const CodeEditor = createReactClass({
|
||||
let cursorPos;
|
||||
let newComment;
|
||||
const selection = this.codeMirror?.getSelection();
|
||||
if(this.props.language === 'gfm'){
|
||||
if(this.isGFM()){
|
||||
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
|
||||
cursorPos = 4;
|
||||
newComment = `<!-- ${selection} -->`;
|
||||
@@ -334,6 +364,7 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeLink : function() {
|
||||
if(!this.isGFM()) return;
|
||||
const isLink = /^\[(.*)\]\((.*)\)$/;
|
||||
const selection = this.codeMirror?.getSelection().trim();
|
||||
let match;
|
||||
@@ -351,7 +382,8 @@ const CodeEditor = createReactClass({
|
||||
},
|
||||
|
||||
makeList : function(listType) {
|
||||
const selectionStart = this.codeMirror?.getCursor('from'), selectionEnd = this.codeMirror?.getCursor('to');
|
||||
if(!this.isGFM()) return;
|
||||
const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
|
||||
this.codeMirror?.setSelection(
|
||||
{ line: selectionStart.line, ch: 0 },
|
||||
{ line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length }
|
||||
|
||||
@@ -86,9 +86,9 @@ const Editor = createReactClass({
|
||||
});
|
||||
}
|
||||
const snippetBar = document.querySelector('.editor > .snippetBar');
|
||||
if (!snippetBar) return;
|
||||
if(!snippetBar) return;
|
||||
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
this.resizeObserver = new ResizeObserver(entries=>{
|
||||
const height = document.querySelector('.editor > .snippetBar').offsetHeight;
|
||||
this.setState({ snippetBarHeight: height });
|
||||
});
|
||||
@@ -117,7 +117,7 @@ const Editor = createReactClass({
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||
if(this.resizeObserver) this.resizeObserver.disconnect();
|
||||
},
|
||||
|
||||
handleControlKeys : function(e){
|
||||
@@ -392,7 +392,7 @@ const Editor = createReactClass({
|
||||
|
||||
isJumping = true;
|
||||
checkIfScrollComplete();
|
||||
if (this.codeEditor.current?.codeMirror) {
|
||||
if(this.codeEditor.current?.codeMirror) {
|
||||
this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
|
||||
}
|
||||
|
||||
@@ -442,6 +442,7 @@ const Editor = createReactClass({
|
||||
<CodeEditor key='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='gfm'
|
||||
tab='brewText'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onBrewChange('text')}
|
||||
@@ -455,6 +456,7 @@ const Editor = createReactClass({
|
||||
<CodeEditor key='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='css'
|
||||
tab='brewStyles'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onBrewChange('style')}
|
||||
@@ -484,6 +486,7 @@ const Editor = createReactClass({
|
||||
<CodeEditor key='codeEditor'
|
||||
ref={this.codeEditor}
|
||||
language='gfm'
|
||||
tab='brewSnippets'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.snippets}
|
||||
onChange={this.props.onBrewChange('snippets')}
|
||||
|
||||
@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
|
||||
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||
}}
|
||||
|
||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||
|
||||
{{artist,bottom:160px,left:100px
|
||||
##### Homebrew Mug
|
||||
@@ -77,16 +77,16 @@ If you wish to sell or in some way gain profit for what's created on this site,
|
||||
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### More Homebrew Resources
|
||||
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
|
||||
{{position:absolute;top:20px;right:20px;width:auto
|
||||
[{height:30px}](https://discord.gg/by3deKx)
|
||||
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||
[{height:30px}](https://discord.gg/by3deKx)
|
||||
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||
}}
|
||||
|
||||
\page
|
||||
@@ -162,7 +162,7 @@ Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You
|
||||
|
||||
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
|
||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export default [
|
||||
icon : 'fas fa-image',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
||||
' src=\'https://homebrewery.naturalcrit.com/assets/catwarrior.jpg\' ',
|
||||
' style=\'width:325px\' />',
|
||||
'Credit: Kyounghwan Kim'
|
||||
].join('\n')
|
||||
@@ -50,7 +50,7 @@ export default [
|
||||
icon : 'fas fa-tree',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
||||
' src=\'https://homebrewery.naturalcrit.com/assets/homebrewerymug.png\' ',
|
||||
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
||||
].join('\n')
|
||||
},
|
||||
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
return dedent`
|
||||
{{frontCover}}
|
||||
|
||||
{{logo }}
|
||||
{{logo }}
|
||||
|
||||
# ${_.sample(titles)}
|
||||
## ${_.sample(subtitles)}
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
${_.sample(footnote)}
|
||||
}}
|
||||
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
|
||||
\page`;
|
||||
},
|
||||
@@ -110,10 +110,10 @@ export default {
|
||||
___
|
||||
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
}}
|
||||
|
||||
{{logo }}
|
||||
{{logo }}
|
||||
|
||||
\page`;
|
||||
},
|
||||
@@ -126,7 +126,7 @@ export default {
|
||||
## ${_.sample(subtitles)}
|
||||
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:10cm,--rotation:180
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
}}
|
||||
|
||||
\page`;
|
||||
@@ -143,10 +143,10 @@ export default {
|
||||
|
||||
For use with any fantasy roleplaying ruleset. Play the best game of your life!
|
||||
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
{position:absolute,bottom:0,left:0,height:100%}
|
||||
|
||||
{{logo
|
||||

|
||||

|
||||
|
||||
Homebrewery.Naturalcrit.com
|
||||
}}`;
|
||||
|
||||
@@ -645,25 +645,25 @@ export default [
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}`
|
||||
 {width:325px,mix-blend-mode:multiply}`
|
||||
},
|
||||
{
|
||||
name : 'Image Wrap Left',
|
||||
icon : 'fac image-wrap-left',
|
||||
gen : dedent`
|
||||
 {width:280px,margin-right:-3cm,wrapLeft}`
|
||||
 {width:280px,margin-right:-3cm,wrapLeft}`
|
||||
},
|
||||
{
|
||||
name : 'Image Wrap Right',
|
||||
icon : 'fac image-wrap-right',
|
||||
gen : dedent`
|
||||
 {width:280px,margin-left:-3cm,wrapRight}`
|
||||
 {width:280px,margin-left:-3cm,wrapRight}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}`
|
||||
},
|
||||
{
|
||||
name : 'Watercolor Splatter',
|
||||
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
center : ()=>{
|
||||
return dedent`
|
||||
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
|
||||
{height:100%}
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||
Use --offsetY to shift the mask up or down
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
}[side];
|
||||
return dedent`
|
||||
{{imageMaskEdge${_.random(1, 8)},--offset:0%,--rotation:${rotation}
|
||||
{height:100%}
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offset to shift the mask away from page center (can use cm instead of %)
|
||||
Use --rotation to set rotation angle in degrees. -->\n\n`;
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
const offsetY = (y == 'top' ? '50%' : '-50%');
|
||||
return dedent`
|
||||
{{imageMaskCorner${_.random(1, 37)},--offsetX:${offsetX},--offsetY:${offsetY},--rotation:0
|
||||
{height:100%}
|
||||
{height:100%}
|
||||
}}
|
||||
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||
Use --offsetY to shift the mask up or down
|
||||
|
||||
@@ -50,10 +50,10 @@ export default {
|
||||
ccbyndBadge : ``,
|
||||
ccbyncndBadge : ``,
|
||||
shadowDarkNotice : `\[Product Name]\ is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.\n`,
|
||||
shadowDarkBlack : `{width:200px}`,
|
||||
shadowDarkWhite : `{width:200px}`,
|
||||
shadowDarkBlack : `{width:200px}`,
|
||||
shadowDarkWhite : `{width:200px}`,
|
||||
bladesDarkNotice : `This work is based on Blades in the Dark \(found at (http://www.bladesinthedark.com/)\), product of One Seven Design, developed and authored by John Harper, and licensed for our use under the Creative Commons Attribution 3.0 Unported license \(http://creativecommons.org/licenses/by/3.0/\).\n`,
|
||||
bladesDarkLogo : ``,
|
||||
bladesDarkLogo : ``,
|
||||
bladesDarkLogoAttribution : `*Blades in the Dark^tm^ is a trademark of One Seven Design. The Forged in the Dark Logo is © One Seven Design, and is used with permission.*`,
|
||||
iconsCompatibility : 'Compatibility with Icons requires Icons Superpowered Roleplaying from Ad Infinitum Adventures. Ad Infinitum Adventures does not guarantee compatibility, and does not endorse this product.',
|
||||
iconsTrademark : 'Icons Superpowered Roleplaying is a trademark of Steve Kenson, published exclusively by Ad Infinitum Adventures. The Icons Superpowered Roleplaying Compatibility Logo is a trademark of Ad Infinitum Adventures and is used under the Icons Superpowered Roleplaying Compatibility License.',
|
||||
|
||||
@@ -101,10 +101,10 @@ export default {
|
||||
},
|
||||
// Verify Logo redistribution
|
||||
greenRoninAgeCreatorsAllianceCover : `Requires the \[Game Title\] Rulebook from Green Ronin Publishing for use.`,
|
||||
greenRoninAgeCreatorsAllianceLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceBlueRoseLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceFantasyAgeCompatible : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceModernAGECompatible : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceBlueRoseLogo : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceFantasyAgeCompatible : `{width:200px}`,
|
||||
greenRoninAgeCreatorsAllianceModernAGECompatible : `{width:200px}`,
|
||||
// Green Ronin's Chronicle - Verify Art and Access
|
||||
greenRoninChronicleSystemGuildColophon : function() {
|
||||
return dedent`
|
||||
@@ -179,10 +179,10 @@ export default {
|
||||
`;
|
||||
},
|
||||
// Verify Logo redistribution
|
||||
monteCookLogoDarkLarge : ``,
|
||||
monteCookLogoDarkSmall : ``,
|
||||
monteCookLogoLightLarge : ``,
|
||||
monteCookLogoLightSmall : ``,
|
||||
monteCookLogoDarkLarge : ``,
|
||||
monteCookLogoDarkSmall : ``,
|
||||
monteCookLogoLightLarge : ``,
|
||||
monteCookLogoLightSmall : ``,
|
||||
// Onyx Path Canis Minor - Verify logos and access
|
||||
onyxPathCanisMinorColophon : function () {
|
||||
return dedent`
|
||||
|
||||
BIN
themes/assets/catwarrior.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
themes/assets/demontemple.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
themes/assets/dragoninflight.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
themes/assets/homebrewerymug.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
themes/assets/mountaincottage.jpg
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
themes/assets/nightchapel.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
themes/assets/shopvials.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |