Add the options page.
This commit is contained in:
		
							parent
							
								
									16ba17eb9c
								
							
						
					
					
						commit
						f870c0bbf6
					
				| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
 | 
				
			||||||
 | 
					  <path fill="#e6deff" d="M0,16 l48,0 l-24,32" />
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 121 B  | 
| 
						 | 
					@ -0,0 +1,216 @@
 | 
				
			||||||
 | 
					import {ConfirmButton} from '@holllo/preact-components';
 | 
				
			||||||
 | 
					import {html} from 'htm/preact';
 | 
				
			||||||
 | 
					import {Component} from 'preact';
 | 
				
			||||||
 | 
					import browser from 'webextension-polyfill';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  matcherTypes,
 | 
				
			||||||
 | 
					  narrowMatcherType,
 | 
				
			||||||
 | 
					  narrowRedirectType,
 | 
				
			||||||
 | 
					  parseRedirect,
 | 
				
			||||||
 | 
					  Redirects,
 | 
				
			||||||
 | 
					  RedirectParameters,
 | 
				
			||||||
 | 
					  redirectTypes,
 | 
				
			||||||
 | 
					} from '../../redirect/exports.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  redirect?: Redirects;
 | 
				
			||||||
 | 
					  removeRedirect: (id: string) => void;
 | 
				
			||||||
 | 
					  saveRedirect: (redirect: Redirects) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type State = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  redirectValue: string;
 | 
				
			||||||
 | 
					} & RedirectParameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Editor extends Component<Props, State> {
 | 
				
			||||||
 | 
					  defaultParameters: RedirectParameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(props: Props) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.defaultParameters = {
 | 
				
			||||||
 | 
					      enabled: true,
 | 
				
			||||||
 | 
					      matcherType: 'hostname',
 | 
				
			||||||
 | 
					      matcherValue: '',
 | 
				
			||||||
 | 
					      redirectType: 'simple',
 | 
				
			||||||
 | 
					      redirectValue: '',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.state = {
 | 
				
			||||||
 | 
					      id: this.props.id,
 | 
				
			||||||
 | 
					      ...this.parametersFromProps(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onInput = (event: Event, input: 'matcher' | 'redirect') => {
 | 
				
			||||||
 | 
					    const target = event.target as HTMLInputElement;
 | 
				
			||||||
 | 
					    const value = target.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (input === 'matcher') {
 | 
				
			||||||
 | 
					      this.setState({matcherValue: value});
 | 
				
			||||||
 | 
					    } else if (input === 'redirect') {
 | 
				
			||||||
 | 
					      this.setState({redirectValue: value});
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      throw new Error(`Unexpected input changed: ${input as string}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onSelectChange = (event: Event, select: 'matcher' | 'redirect') => {
 | 
				
			||||||
 | 
					    const target = event.target as HTMLSelectElement;
 | 
				
			||||||
 | 
					    const value = target.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (select === 'matcher' && narrowMatcherType(value)) {
 | 
				
			||||||
 | 
					      this.setState({matcherType: value});
 | 
				
			||||||
 | 
					    } else if (select === 'redirect' && narrowRedirectType(value)) {
 | 
				
			||||||
 | 
					      this.setState({redirectType: value});
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      throw new Error(`${value} is not a valid MatcherType or RedirectType`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMatcherInput = (event: Event) => {
 | 
				
			||||||
 | 
					    this.onInput(event, 'matcher');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMatcherTypeChange = (event: Event) => {
 | 
				
			||||||
 | 
					    this.onSelectChange(event, 'matcher');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onRedirectInput = (event: Event) => {
 | 
				
			||||||
 | 
					    this.onInput(event, 'redirect');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onRedirectTypeChange = (event: Event) => {
 | 
				
			||||||
 | 
					    this.onSelectChange(event, 'redirect');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  parametersFromProps = (): RedirectParameters => {
 | 
				
			||||||
 | 
					    const redirect = this.props.redirect;
 | 
				
			||||||
 | 
					    const parameters = redirect?.parameters ?? {...this.defaultParameters};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      enabled: parameters.enabled,
 | 
				
			||||||
 | 
					      matcherType: parameters.matcherType,
 | 
				
			||||||
 | 
					      matcherValue: parameters.matcherValue,
 | 
				
			||||||
 | 
					      redirectType: parameters.redirectType,
 | 
				
			||||||
 | 
					      redirectValue: parameters.redirectValue,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  parseRedirect = (): Redirects => {
 | 
				
			||||||
 | 
					    const redirect = parseRedirect(this.state, this.props.id);
 | 
				
			||||||
 | 
					    if (redirect === undefined) {
 | 
				
			||||||
 | 
					      throw new Error('Failed to parse redirect');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return redirect;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prepareForStorage = (
 | 
				
			||||||
 | 
					    parameters: RedirectParameters,
 | 
				
			||||||
 | 
					  ): Record<string, RedirectParameters> => {
 | 
				
			||||||
 | 
					    const storage: Record<string, RedirectParameters> = {};
 | 
				
			||||||
 | 
					    storage[this.props.id] = parameters;
 | 
				
			||||||
 | 
					    return storage;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  remove = async () => {
 | 
				
			||||||
 | 
					    await browser.storage.local.remove(this.props.id);
 | 
				
			||||||
 | 
					    this.props.removeRedirect(this.props.id);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  save = async () => {
 | 
				
			||||||
 | 
					    const redirect = this.parseRedirect();
 | 
				
			||||||
 | 
					    await browser.storage.local.set(
 | 
				
			||||||
 | 
					      this.prepareForStorage(redirect.parameters),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    this.props.saveRedirect(redirect);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toggleEnabled = async () => {
 | 
				
			||||||
 | 
					    const enabled = !this.state.enabled;
 | 
				
			||||||
 | 
					    const storage = this.prepareForStorage(this.parametersFromProps());
 | 
				
			||||||
 | 
					    storage[this.props.id].enabled = enabled;
 | 
				
			||||||
 | 
					    await browser.storage.local.set(storage);
 | 
				
			||||||
 | 
					    this.setState({enabled});
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const {enabled, matcherType, matcherValue, redirectType, redirectValue} =
 | 
				
			||||||
 | 
					      this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const matcherTypeOptions = matcherTypes.map(
 | 
				
			||||||
 | 
					      (value) => html`<option value=${value}>${value}</option>`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const redirectTypeOptions = redirectTypes.map(
 | 
				
			||||||
 | 
					      (value) => html`<option value=${value}>${value}</option>`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <div class="editor">
 | 
				
			||||||
 | 
					        <select
 | 
				
			||||||
 | 
					          class="select"
 | 
				
			||||||
 | 
					          id="matcher-type"
 | 
				
			||||||
 | 
					          title="Matcher Type"
 | 
				
			||||||
 | 
					          value=${matcherType}
 | 
				
			||||||
 | 
					          onChange=${this.onMatcherTypeChange}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          ${matcherTypeOptions}
 | 
				
			||||||
 | 
					        </select>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          class="input"
 | 
				
			||||||
 | 
					          id="matcher-value"
 | 
				
			||||||
 | 
					          onInput=${this.onMatcherInput}
 | 
				
			||||||
 | 
					          placeholder="Matcher Value"
 | 
				
			||||||
 | 
					          title="Matcher Value"
 | 
				
			||||||
 | 
					          value=${matcherValue}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span class="arrow-span">→</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <select
 | 
				
			||||||
 | 
					          class="select"
 | 
				
			||||||
 | 
					          id="redirect-type"
 | 
				
			||||||
 | 
					          title="Redirect Type"
 | 
				
			||||||
 | 
					          value=${redirectType}
 | 
				
			||||||
 | 
					          onChange=${this.onRedirectTypeChange}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          ${redirectTypeOptions}
 | 
				
			||||||
 | 
					        </select>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          class="input"
 | 
				
			||||||
 | 
					          id="redirect-value"
 | 
				
			||||||
 | 
					          onInput=${this.onRedirectInput}
 | 
				
			||||||
 | 
					          placeholder="Redirect Value"
 | 
				
			||||||
 | 
					          title="Redirect Value"
 | 
				
			||||||
 | 
					          value=${redirectValue}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <${ConfirmButton}
 | 
				
			||||||
 | 
					          attributes=${{title: 'Remove Redirect'}}
 | 
				
			||||||
 | 
					          class="button destructive"
 | 
				
			||||||
 | 
					          click=${this.remove}
 | 
				
			||||||
 | 
					          confirmClass="confirm"
 | 
				
			||||||
 | 
					          confirmText="✓"
 | 
				
			||||||
 | 
					          text="✗"
 | 
				
			||||||
 | 
					          timeout=${5 * 1000}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <button class="button" title="Save Redirect" onClick=${this.save}>
 | 
				
			||||||
 | 
					          💾
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					          class="button ${enabled ? 'enabled' : 'disabled'}"
 | 
				
			||||||
 | 
					          title="${enabled ? 'Currently Enabled' : 'Currently Disabled'}"
 | 
				
			||||||
 | 
					          onClick=${this.toggleEnabled}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          ${enabled ? '●' : '○'}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					import {PrivacyLink} from '@holllo/preact-components';
 | 
				
			||||||
 | 
					import {html} from 'htm/preact';
 | 
				
			||||||
 | 
					import {Component} from 'preact';
 | 
				
			||||||
 | 
					import browser from 'webextension-polyfill';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class PageFooter extends Component {
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const manifest = browser.runtime.getManifest();
 | 
				
			||||||
 | 
					    const version = manifest.version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const donateAttributes = {
 | 
				
			||||||
 | 
					      href: 'https://liberapay.com/Holllo',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const donateLink = html`
 | 
				
			||||||
 | 
					      <${PrivacyLink} attributes="${donateAttributes}">Donate<//>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const versionLinkAttributes = {
 | 
				
			||||||
 | 
					      href: `https://git.bauke.xyz/Holllo/re-nav/releases/tag/${version}`,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const versionLink = html`
 | 
				
			||||||
 | 
					      <${PrivacyLink} attributes=${versionLinkAttributes}>v${version}<//>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <footer class="page-footer">
 | 
				
			||||||
 | 
					        <p>
 | 
				
			||||||
 | 
					          ${donateLink} 💖 ${versionLink} © Holllo — Free and open-source,
 | 
				
			||||||
 | 
					          forever.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </footer>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					import {html} from 'htm/preact';
 | 
				
			||||||
 | 
					import {Component} from 'preact';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class PageHeader extends Component {
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <header class="page-header">
 | 
				
			||||||
 | 
					        <h1>
 | 
				
			||||||
 | 
					          <img alt="Re-Nav Logo" src="/assets/re-nav.png" />
 | 
				
			||||||
 | 
					          Re-Nav
 | 
				
			||||||
 | 
					        </h1>
 | 
				
			||||||
 | 
					      </header>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,146 @@
 | 
				
			||||||
 | 
					import {html} from 'htm/preact';
 | 
				
			||||||
 | 
					import {Component} from 'preact';
 | 
				
			||||||
 | 
					import browser from 'webextension-polyfill';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  parseRedirect,
 | 
				
			||||||
 | 
					  Redirect,
 | 
				
			||||||
 | 
					  Redirects,
 | 
				
			||||||
 | 
					  SimpleRedirect,
 | 
				
			||||||
 | 
					} from '../../redirect/exports.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Editor from './editor.js';
 | 
				
			||||||
 | 
					import Usage from './usage.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = Record<string, unknown>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type State = {
 | 
				
			||||||
 | 
					  redirects: Redirects[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class PageMain extends Component<Props, State> {
 | 
				
			||||||
 | 
					  constructor(props: Props) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.state = {
 | 
				
			||||||
 | 
					      redirects: [],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async componentDidMount() {
 | 
				
			||||||
 | 
					    const redirects: Redirects[] = [];
 | 
				
			||||||
 | 
					    for (const [id, parameters] of Object.entries(
 | 
				
			||||||
 | 
					      await browser.storage.local.get(),
 | 
				
			||||||
 | 
					    )) {
 | 
				
			||||||
 | 
					      const redirect = parseRedirect(parameters, id);
 | 
				
			||||||
 | 
					      if (redirect === undefined) {
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      redirects.push(redirect);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sort the redirects by:
 | 
				
			||||||
 | 
					    // * Matcher Type
 | 
				
			||||||
 | 
					    // * then Matcher Value
 | 
				
			||||||
 | 
					    // * then Redirect Type
 | 
				
			||||||
 | 
					    // * finally Redirect Value
 | 
				
			||||||
 | 
					    redirects.sort((a, b) => {
 | 
				
			||||||
 | 
					      const {
 | 
				
			||||||
 | 
					        matcherType: mTypeA,
 | 
				
			||||||
 | 
					        matcherValue: mValueA,
 | 
				
			||||||
 | 
					        redirectType: rTypeA,
 | 
				
			||||||
 | 
					        redirectValue: rValueA,
 | 
				
			||||||
 | 
					      } = a.parameters;
 | 
				
			||||||
 | 
					      const {
 | 
				
			||||||
 | 
					        matcherType: mTypeB,
 | 
				
			||||||
 | 
					        matcherValue: mValueB,
 | 
				
			||||||
 | 
					        redirectType: rTypeB,
 | 
				
			||||||
 | 
					        redirectValue: rValueB,
 | 
				
			||||||
 | 
					      } = b.parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (mTypeA !== mTypeB) {
 | 
				
			||||||
 | 
					        return mTypeA.localeCompare(mTypeB);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (mValueA !== mValueB) {
 | 
				
			||||||
 | 
					        return mValueA.localeCompare(mValueB);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (rTypeA !== rTypeB) {
 | 
				
			||||||
 | 
					        return rTypeA.localeCompare(rTypeB);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return rValueA.localeCompare(rValueB);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    this.setState({redirects});
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addNewRedirect = () => {
 | 
				
			||||||
 | 
					    this.setState({
 | 
				
			||||||
 | 
					      redirects: [
 | 
				
			||||||
 | 
					        new SimpleRedirect({
 | 
				
			||||||
 | 
					          enabled: true,
 | 
				
			||||||
 | 
					          matcherType: 'hostname',
 | 
				
			||||||
 | 
					          matcherValue: 'example.com',
 | 
				
			||||||
 | 
					          redirectType: 'simple',
 | 
				
			||||||
 | 
					          redirectValue: 'example.org',
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        ...this.state.redirects,
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeRedirect = (id: string) => {
 | 
				
			||||||
 | 
					    this.setState({
 | 
				
			||||||
 | 
					      redirects: this.state.redirects.filter((redirect) => redirect.id !== id),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  saveRedirect = (redirect: Redirects) => {
 | 
				
			||||||
 | 
					    const redirectIndex = this.state.redirects.findIndex(
 | 
				
			||||||
 | 
					      (found) => found.id === redirect.id,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (redirectIndex === -1) {
 | 
				
			||||||
 | 
					      this.setState({
 | 
				
			||||||
 | 
					        redirects: [redirect, ...this.state.redirects],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const redirects = [...this.state.redirects];
 | 
				
			||||||
 | 
					      redirects[redirectIndex] = redirect;
 | 
				
			||||||
 | 
					      this.setState({redirects});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const editors = this.state.redirects.map(
 | 
				
			||||||
 | 
					      (redirect) =>
 | 
				
			||||||
 | 
					        html`
 | 
				
			||||||
 | 
					          <${Editor}
 | 
				
			||||||
 | 
					            key=${redirect.id}
 | 
				
			||||||
 | 
					            id=${redirect.id}
 | 
				
			||||||
 | 
					            redirect=${redirect}
 | 
				
			||||||
 | 
					            removeRedirect=${this.removeRedirect}
 | 
				
			||||||
 | 
					            saveRedirect=${this.saveRedirect}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        `,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (editors.length === 0) {
 | 
				
			||||||
 | 
					      this.addNewRedirect();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <main class="page-main">
 | 
				
			||||||
 | 
					        <div class="editors">
 | 
				
			||||||
 | 
					          <button class="button new-redirect" onClick=${this.addNewRedirect}>
 | 
				
			||||||
 | 
					            Add New Redirect
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ${editors}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <${Usage} />
 | 
				
			||||||
 | 
					      </main>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,157 @@
 | 
				
			||||||
 | 
					import {html} from 'htm/preact';
 | 
				
			||||||
 | 
					import {Component} from 'preact';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Usage extends Component {
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <details class="usage">
 | 
				
			||||||
 | 
					        <summary>How do I use Re-Nav?</summary>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>Creating redirects:</p>
 | 
				
			||||||
 | 
					        <ul>
 | 
				
			||||||
 | 
					          <li>Click the green "Add new redirect" button.</li>
 | 
				
			||||||
 | 
					          <li>Select a matcher type and enter what it should match on.</li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Select a redirect type and enter where you want to be redirected.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            See the "Matchers" and "Redirects" sections below for lists of
 | 
				
			||||||
 | 
					            everything available with examples.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>Using redirects:</p>
 | 
				
			||||||
 | 
					        <ul>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Any time you are navigated to a link by your browser, the URL will
 | 
				
			||||||
 | 
					            first be checked and you will be redirected automatically.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>Editing redirects:</p>
 | 
				
			||||||
 | 
					        <ul>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Changes to redirects are only saved when you click the save button.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            To enable or disable a redirect, click the button with the circle.
 | 
				
			||||||
 | 
					            If it's filled in the redirect is enabled.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					          <li>To remove a redirect click the red button with the ✗ twice.</li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>Some miscellaneous notes:</p>
 | 
				
			||||||
 | 
					        <ul>
 | 
				
			||||||
 | 
					          <li>Only URLs starting with "http" will be checked.</li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Navigation events won't be checked if it has been less than 100
 | 
				
			||||||
 | 
					            milliseconds since the last successful redirect.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            A redirect will be cancelled if the exact same redirect happened
 | 
				
			||||||
 | 
					            less than 30 seconds ago. This acts as a quick bypass so you don't
 | 
				
			||||||
 | 
					            have to disable redirects in the options page whenever you don't
 | 
				
			||||||
 | 
					            want to be redirected.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <p>As a quick-start you can also insert the examples from below:</p>
 | 
				
			||||||
 | 
					        <ul>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Note that this will reload the page so make sure your redirects have
 | 
				
			||||||
 | 
					            been saved.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            <button class="button" onClick=${window.Holllo.insertExamples}>
 | 
				
			||||||
 | 
					              Insert Examples
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					      </details>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <details class="usage table">
 | 
				
			||||||
 | 
					        <summary>Matchers</summary>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <table>
 | 
				
			||||||
 | 
					          <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <th>Type</th>
 | 
				
			||||||
 | 
					              <th>Match Directive</th>
 | 
				
			||||||
 | 
					              <th>Match Examples</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </thead>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <tbody>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td class="bold center-text" rowspan="2">Hostname</td>
 | 
				
			||||||
 | 
					              <td class="center-text" rowspan="2">tildes.net</td>
 | 
				
			||||||
 | 
					              <td>http://<b>www.tildes.net</b>/<sup>1</sup></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td>https://<b>tildes.net</b>/~creative.timasomo</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr class="alt">
 | 
				
			||||||
 | 
					              <td class="bold center-text" rowspan="2">Regex</td>
 | 
				
			||||||
 | 
					              <td class="center-text">HOL{3}O</td>
 | 
				
			||||||
 | 
					              <td>https://git.bauke.xyz/<b>holllo</b><sup>2</sup></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr class="alt">
 | 
				
			||||||
 | 
					              <td class="center-text">^https?://www\\.holllo\\.org/?$</td>
 | 
				
			||||||
 | 
					              <td><b>https://www.holllo.org/</b></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ol class="footnotes">
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Hostname matchers always remove "www." automatically, for
 | 
				
			||||||
 | 
					            convenience.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					          <li>
 | 
				
			||||||
 | 
					            Regular expressions are always tested with global and
 | 
				
			||||||
 | 
					            case-insensitive flags enabled.
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        </ol>
 | 
				
			||||||
 | 
					      </details>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <details class="usage table">
 | 
				
			||||||
 | 
					        <summary>Redirects</summary>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <table>
 | 
				
			||||||
 | 
					          <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <th>Type</th>
 | 
				
			||||||
 | 
					              <th>Change To</th>
 | 
				
			||||||
 | 
					              <th>Example URL</th>
 | 
				
			||||||
 | 
					              <th>Redirected URL<sup>1</sup></th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </thead>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <tbody>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td class="bold center-text" rowspan="2">Hostname</td>
 | 
				
			||||||
 | 
					              <td>nitter.net</td>
 | 
				
			||||||
 | 
					              <td>https://twitter.com/therealTDH</td>
 | 
				
			||||||
 | 
					              <td>https://<b>nitter.net</b>/therealTDH</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td>r.nf</td>
 | 
				
			||||||
 | 
					              <td>https://www.reddit.com/r/TheDearHunter</td>
 | 
				
			||||||
 | 
					              <td>https://<b>r.nf</b>/r/TheDearHunter</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr class="alt">
 | 
				
			||||||
 | 
					              <td class="bold center-text">Simple</td>
 | 
				
			||||||
 | 
					              <td>https://holllo.org</td>
 | 
				
			||||||
 | 
					              <td>https://holllo.org/home</td>
 | 
				
			||||||
 | 
					              <td><b>https://holllo.org</b></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ol class="footnotes">
 | 
				
			||||||
 | 
					          <li>The bold highlighted text shows what will be changed.</li>
 | 
				
			||||||
 | 
					        </ol>
 | 
				
			||||||
 | 
					      </details>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import {Redirect, RedirectParameters} from '../redirect/base.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const examples: RedirectParameters[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					    matcherType: 'hostname',
 | 
				
			||||||
 | 
					    matcherValue: 'twitter.com',
 | 
				
			||||||
 | 
					    redirectType: 'hostname',
 | 
				
			||||||
 | 
					    redirectValue: 'nitter.net',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					    matcherType: 'hostname',
 | 
				
			||||||
 | 
					    matcherValue: 'reddit.com',
 | 
				
			||||||
 | 
					    redirectType: 'hostname',
 | 
				
			||||||
 | 
					    redirectValue: 'r.nf',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					    matcherType: 'regex',
 | 
				
			||||||
 | 
					    matcherValue: '^https?://holllo\\.org/renav/?$',
 | 
				
			||||||
 | 
					    redirectType: 'simple',
 | 
				
			||||||
 | 
					    redirectValue: 'https://holllo.org/re-nav',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function generateExamples(): Record<string, RedirectParameters> {
 | 
				
			||||||
 | 
					  const storage: Record<string, RedirectParameters> = {};
 | 
				
			||||||
 | 
					  for (const example of examples) {
 | 
				
			||||||
 | 
					    const id = Redirect.generateId();
 | 
				
			||||||
 | 
					    storage[id] = example;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return storage;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					  <title>Re-Nav</title>
 | 
				
			||||||
 | 
					  <link rel="shortcut icon" href="/assets/re-nav.png" type="image/png">
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="./index.scss">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<body class="love">
 | 
				
			||||||
 | 
					  <noscript>
 | 
				
			||||||
 | 
					    This WebExtension doesn't work without JavaScript enabled, sorry! 😭
 | 
				
			||||||
 | 
					  </noscript>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <script type="module" src="./index.ts"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					@use '../../node_modules/modern-normalize/modern-normalize.css';
 | 
				
			||||||
 | 
					@use 'scss/reset';
 | 
				
			||||||
 | 
					@use 'scss/mixins';
 | 
				
			||||||
 | 
					@use 'scss/love';
 | 
				
			||||||
 | 
					@use 'scss/components/page-header';
 | 
				
			||||||
 | 
					@use 'scss/components/page-main';
 | 
				
			||||||
 | 
					@use 'scss/components/page-footer';
 | 
				
			||||||
 | 
					@use 'scss/components/editor';
 | 
				
			||||||
 | 
					@use 'scss/components/usage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html {
 | 
				
			||||||
 | 
					  font-size: 62.5%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  background-color: var(--db-1);
 | 
				
			||||||
 | 
					  color: var(--df-1);
 | 
				
			||||||
 | 
					  font-size: 1.5rem;
 | 
				
			||||||
 | 
					  padding: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a {
 | 
				
			||||||
 | 
					  color: var(--da-3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    color: var(--df-2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:focus {
 | 
				
			||||||
 | 
					  outline-color: var(--df-1);
 | 
				
			||||||
 | 
					  outline-width: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bold {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.center-text {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					import {html} from 'htm/preact';
 | 
				
			||||||
 | 
					import {Component, render} from 'preact';
 | 
				
			||||||
 | 
					import browser from 'webextension-polyfill';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {PageFooter} from './components/page-footer.js';
 | 
				
			||||||
 | 
					import {PageHeader} from './components/page-header.js';
 | 
				
			||||||
 | 
					import {PageMain} from './components/page-main.js';
 | 
				
			||||||
 | 
					import {generateExamples} from './examples.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
 | 
					  window.Holllo = {
 | 
				
			||||||
 | 
					    async insertExamples() {
 | 
				
			||||||
 | 
					      await browser.storage.local.set(generateExamples());
 | 
				
			||||||
 | 
					      location.reload();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render(html`<${OptionsPage} />`, document.body);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OptionsPage extends Component {
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <${PageHeader} />
 | 
				
			||||||
 | 
					      <${PageMain} />
 | 
				
			||||||
 | 
					      <${PageFooter} />
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					.editor {
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  background-color: var(--db-2);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  padding: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .arrow-span {
 | 
				
			||||||
 | 
					    aspect-ratio: 1;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .button {
 | 
				
			||||||
 | 
					    aspect-ratio: 1;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    margin-left: 8px;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.enabled {
 | 
				
			||||||
 | 
					      background-color: var(--da-4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.disabled {
 | 
				
			||||||
 | 
					      background-color: var(--da-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .input {
 | 
				
			||||||
 | 
					    background-color: var(--db-1);
 | 
				
			||||||
 | 
					    border: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    color: var(--df-1);
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .select {
 | 
				
			||||||
 | 
					    appearance: none;
 | 
				
			||||||
 | 
					    background: url('../../../assets/down-arrow.svg') no-repeat center right;
 | 
				
			||||||
 | 
					    background-size: 1.5rem;
 | 
				
			||||||
 | 
					    background-color: var(--db-1);
 | 
				
			||||||
 | 
					    border: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    border-radius: 0;
 | 
				
			||||||
 | 
					    color: var(--df-1);
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    margin-right: 8px;
 | 
				
			||||||
 | 
					    padding: 4px 24px 4px 4px;
 | 
				
			||||||
 | 
					    width: fit-content;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					@use '../mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-footer {
 | 
				
			||||||
 | 
					  @include mixins.responsive-container;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  border: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					  margin-bottom: 16px;
 | 
				
			||||||
 | 
					  padding: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					@use '../mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-header {
 | 
				
			||||||
 | 
					  @include mixins.responsive-container;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  border: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					  margin-bottom: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  h1 {
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  img {
 | 
				
			||||||
 | 
					    background-color: var(--df-2);
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    height: 4.5rem;
 | 
				
			||||||
 | 
					    margin-right: 1rem;
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    width: 4.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					@use '../mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-main {
 | 
				
			||||||
 | 
					  @include mixins.responsive-container;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					  margin-bottom: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .button {
 | 
				
			||||||
 | 
					    background-color: var(--da-3);
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    color: var(--db-1);
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.destructive {
 | 
				
			||||||
 | 
					      background-color: var(--da-1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.new-redirect {
 | 
				
			||||||
 | 
					      background-color: var(--da-4);
 | 
				
			||||||
 | 
					      padding: 8px;
 | 
				
			||||||
 | 
					      width: fit-content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      background-color: var(--df-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .editors {
 | 
				
			||||||
 | 
					    border: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    gap: 16px;
 | 
				
			||||||
 | 
					    padding: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,95 @@
 | 
				
			||||||
 | 
					.usage {
 | 
				
			||||||
 | 
					  border: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &[open] {
 | 
				
			||||||
 | 
					    summary {
 | 
				
			||||||
 | 
					      background-color: var(--df-2);
 | 
				
			||||||
 | 
					      color: var(--db-1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(.table) summary {
 | 
				
			||||||
 | 
					      margin-bottom: 16px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    > :not(summary) {
 | 
				
			||||||
 | 
					      padding-left: 16px;
 | 
				
			||||||
 | 
					      padding-right: 16px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  summary {
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    padding: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      background-color: var(--df-1);
 | 
				
			||||||
 | 
					      color: var(--db-1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ul {
 | 
				
			||||||
 | 
					    list-style: square;
 | 
				
			||||||
 | 
					    margin: 4px 0 2rem 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  table {
 | 
				
			||||||
 | 
					    border-collapse: collapse;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      border-bottom: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  th {
 | 
				
			||||||
 | 
					    border-bottom: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  td,
 | 
				
			||||||
 | 
					  th {
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      border-right: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  tr {
 | 
				
			||||||
 | 
					    &.alt {
 | 
				
			||||||
 | 
					      background-color: var(--db-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      border-bottom: 1px solid var(--df-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    td {
 | 
				
			||||||
 | 
					      padding: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      b {
 | 
				
			||||||
 | 
					        color: var(--da-3);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .footnotes {
 | 
				
			||||||
 | 
					    margin-left: 12px;
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    code {
 | 
				
			||||||
 | 
					      background-color: var(--db-2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    li {
 | 
				
			||||||
 | 
					      margin-bottom: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:last-child {
 | 
				
			||||||
 | 
					        margin-bottom: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .button {
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					  The Love Theme CSS Custom Properties
 | 
				
			||||||
 | 
					  https://love.holllo.cc - version 0.1.0
 | 
				
			||||||
 | 
					  MIT license
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.love {
 | 
				
			||||||
 | 
					  /* Love Dark */
 | 
				
			||||||
 | 
					  --df-1: #f2efff;
 | 
				
			||||||
 | 
					  --df-2: #e6deff;
 | 
				
			||||||
 | 
					  --db-1: #1f1731;
 | 
				
			||||||
 | 
					  --db-2: #2a2041;
 | 
				
			||||||
 | 
					  --da-1: #f99fb1;
 | 
				
			||||||
 | 
					  --da-2: #faa56c;
 | 
				
			||||||
 | 
					  --da-3: #d2b83a;
 | 
				
			||||||
 | 
					  --da-4: #96c839;
 | 
				
			||||||
 | 
					  --da-5: #3bd18a;
 | 
				
			||||||
 | 
					  --da-6: #3ecdbf;
 | 
				
			||||||
 | 
					  --da-7: #41c8e5;
 | 
				
			||||||
 | 
					  --da-8: #98b9f8;
 | 
				
			||||||
 | 
					  --da-9: #d5a6f8;
 | 
				
			||||||
 | 
					  --da-10: #f99add;
 | 
				
			||||||
 | 
					  --dg-1: #e2e2e2;
 | 
				
			||||||
 | 
					  --dg-2: #c6c6c6;
 | 
				
			||||||
 | 
					  --dg-3: #ababab;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Love Light */
 | 
				
			||||||
 | 
					  --lf-1: #1f1731;
 | 
				
			||||||
 | 
					  --lf-2: #2a2041;
 | 
				
			||||||
 | 
					  --lb-1: #f2efff;
 | 
				
			||||||
 | 
					  --lb-2: #e6deff;
 | 
				
			||||||
 | 
					  --la-1: #8b123c;
 | 
				
			||||||
 | 
					  --la-2: #6a3b11;
 | 
				
			||||||
 | 
					  --la-3: #514610;
 | 
				
			||||||
 | 
					  --la-4: #384d10;
 | 
				
			||||||
 | 
					  --la-5: #115133;
 | 
				
			||||||
 | 
					  --la-6: #124f49;
 | 
				
			||||||
 | 
					  --la-7: #144d5a;
 | 
				
			||||||
 | 
					  --la-8: #17477e;
 | 
				
			||||||
 | 
					  --la-9: #6f1995;
 | 
				
			||||||
 | 
					  --la-10: #81156a;
 | 
				
			||||||
 | 
					  --lg-1: #1b1b1b;
 | 
				
			||||||
 | 
					  --lg-2: #303030;
 | 
				
			||||||
 | 
					  --lg-3: #474747;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					@use 'variables';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin responsive-container {
 | 
				
			||||||
 | 
					  margin-left: auto;
 | 
				
			||||||
 | 
					  margin-right: auto;
 | 
				
			||||||
 | 
					  width: variables.$large-breakpoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: variables.$large-breakpoint) {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					h1,
 | 
				
			||||||
 | 
					h2,
 | 
				
			||||||
 | 
					h3,
 | 
				
			||||||
 | 
					h4,
 | 
				
			||||||
 | 
					h5,
 | 
				
			||||||
 | 
					ol,
 | 
				
			||||||
 | 
					ul,
 | 
				
			||||||
 | 
					li,
 | 
				
			||||||
 | 
					p {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					$small-breakpoint: 600px;
 | 
				
			||||||
 | 
					$medium-breakpoint: 900px;
 | 
				
			||||||
 | 
					$large-breakpoint: 1200px;
 | 
				
			||||||
 | 
					$extra-large-breakpoint: 1800px;
 | 
				
			||||||
| 
						 | 
					@ -16,5 +16,11 @@ declare global {
 | 
				
			||||||
    readonly VITE_BROWSER: 'chromium' | 'firefox';
 | 
					    readonly VITE_BROWSER: 'chromium' | 'firefox';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  interface Window {
 | 
				
			||||||
 | 
					    Holllo: {
 | 
				
			||||||
 | 
					      insertExamples(): Promise<void>;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type HtmComponent = ReturnType<typeof html>;
 | 
					  type HtmComponent = ReturnType<typeof html>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Reference in New Issue