Git Product home page Git Product logo

css-blocks's People

Contributors

alonski avatar amiller-gh avatar bitttttten avatar brianchung808 avatar chriseppstein avatar chrisrng avatar danielruf avatar elnee avatar fabiomcosta avatar flawyte avatar forsakenharmony avatar greenkeeper[bot] avatar jonathantneal avatar josemarluedke avatar kgrz avatar knownasilya avatar liamross avatar mike-north avatar nickiaconis avatar ramitha avatar sandiiarov avatar snyk-bot avatar styfle avatar timlindvall avatar tomdale avatar wodin avatar zephraph avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

css-blocks's Issues

[css-blocks] synthetic conflicts

There should be a way to create resolution constraints that span properties that don't naturally conflict in css. This can be used to implement things like accessibility constraints for contrast between foreground and background colors.

Such a capability should be extensible within an application or css library that is built on css-blocks.

Conflict Resolver API Changes

The current resolver resolves all selectors that match the target block object in the key selector. This means that states targeting that same object get overridden too. That seemed like a good idea, but it turns out to be sub-optimal for many use cases, especially inheritance (which is a resolution abstraction)

A new syntax for resolution is documented in the readme and needs to be implemented.

https://github.com/css-blocks/css-blocks/blob/master/README.md#resolving-state-conflicts

Support interoperableCSS properly

For existing webpack apps we should explore whether we can build an iCSS interface to css-blocks that works well. We had an early implementation of this, but I am no longer confident that it works sanely and I think re-implementing it from scratch on top of our rewriter code (or a common shared implementation) would be for the best.

TypeScript type checking for css-blocks as imported modules.

Each CSS block file can be represented as a collection of TypeScript classes.

.root { block-name: a-css-block; }
[state|a-block-state] {}
.a-thing {}
.a-thing[state|active] {}
.a-thing[state|my-theme=red] {}
.a-thing[state|my-theme=blue] {}
// these would be defined statically for all blocks and imported
type OptimizedClassNames = string[];
export interface BlockClass {
}
export type BooleanBlockState = () => OptimizedClassNames;
export type ExclusiveBlockState = (value: string) => OptimizedClassNames;
export type BlockState = BooleanBlockState | ExclusiveBlockState;
// type for this block
export class ACssBlock implements BlockClass {
  root: this;
  active: () => OptimizedClassNames;
  "a-thing": ACssBlock.AThingClass;
  [name: string]: BlockClass | BlockState;
}
export namespace ACssBlock {
   export class AThingClass implements BlockClass {
    [name: string]: BlockState;
     "my-theme": (value: "red" | "blue") => OptimizedClassNames;
   }
}
// usage example:
function foo(a: ACssBlock): OptimizedClassNames {
  return a["a-thing"]["my-theme"]("red");
}

TODO:

  • Further examples of block interfaces and inheritance are needed.
  • Figure out how to create indexed types with only a limited set of indexed names.

Proposal: Binary Encoding of Boolean Expression Shapes

Problem

  1. Currently, pushing styling logic to the templates bloats compiled template size and, in some cases, results in a net app size increase. We need a more efficient way to encode boolean logic back into the template to reduce template bloat.
  2. Template rewriters have the unnecessary responsibility of translating boolean expression objects emitted by opticss to the css-blocks runtime helper syntax. Is is possible to remove this overhead and push responsibility back to css-blocks core.

Proposal

We can greatly compress the css-blocks runtime by treating the runtime helper as a projection of arguments over unique boolean expression "shapes".

To maximize space efficiency we store these boolean expression instructions as a list of binary opcodes. All required opcodes for boolean expression evaulation may be represented using just three (3) bits:

const OP_CODES = {
  0: 'OPEN',     // 000
  1: 'VAL',      // 001
  2: 'NOT',      // 010
  3: 'OR',       // 011
  4: 'AND',      // 100
  5: 'EQUAL',    // 101
  6: '---',      // 110
  7: 'CLOSE'     // 111
};

Note: The VAL opcode is always followed by an integer representing the index of the dynamic value passed to our helper it represents. The number of bits to look at to fetch an index is determined by the number of dynamic expressions passed to ensure minimal size. All other opcodes have no special concerns or lookahead.

The following boolean expression:

!(exp1 || exp2)

May be compiled to the following opcodes:

let opcodes = "NOT OPEN VAL 0 OR VAL 1 CLOSE";

And then be represented by the following binary string:

let binaryOpcodes = "010 000 001 0 011 001 1 111";

Encoded shape expressions can be delivered to the runtime helper as an array of base 36 encoded Uint32 integers. The binary opcode order is reversed when inserted into base 36 integers to a) reduce encoded size when delivered to the browser and b) simplify the runtime function implementation.

Continuing the previous example, the above binary opcodes can be delivered as base 36 encoded Uint32 integers by undergoing the following transformations:

// Original binary opcode list:
// 010 000 001 0 011 001 1 111
let integer = parseInt("000000000000 111 1 001 011 0 001 000 010", 2); // 994370
let encoded = integer.toString(36); // "lb9e"

When a set of binary opcodes exceed the 32 bit limit, the opcodes overflow into the next encoded integer delivered in the expression shape's array.

To account for the logic required by many classes that must be applied in a single runtime call to an individual element, the boolean expressions for multiple classes may be encoded into a single expression shape by delineating standalone expressions with an extra CLOSE opcode, demonstrated below.

Note: Because expression shapes are encoded as strings we have the potential to reap gzip/brotli benefits if many identical expression shapes are reused across the app.

The runtime helper public interface now takes the following shape:

function runtime(shape: string[], classes: string[], expressions: boolean[]) => string;

An example with logic for multiple classes call may look like:

// Original: objstr({ class1: expr1 == expr2, class2: expr1 !== expr2 })
// VAL 0 EQUALS VAL 1 CLOSE VAL 0 NOT EQUALS VAL 1 
// 001 0 101 001 1 111 001 0 010 101 001 1 
// 162036945 
// "2oh0i9"
runtime(["2oh0i9"], ["class1", "class2"], [expr1, expr2]);

Here we encode the boolean expressions for two classes. class1 should be displayed when expr1 and expr2 are equal. class2 should be displayed when expr1 and expr2 are not equal.

In practice, the runtime implementation for this type of binary encoding system is very fast. An opcode parser may look something like this:

const INT_SIZE = 32;

function computeStyles(shape, classes, expressions) {

  // We dynamically determine the variable window size based on the number of
  // expressions it is possible to reference. It is the compiler's responsibility
  // to guarentee the expression shape matches at build time.
  const VAR_SIZE = ~~Math.log2(expressions.length - 1) + 1;

  let klass      = 0,     // Current class we are determining state of
      opcode     = null,  // Current opcode to evaluate.
      lookahead  = null,  // This is a single lookahead parser โ€“ lookahead opcode will be stored here.
      step       = 0,     // Character count used for opcode discovery
      invertNext = false, // Should we invert the next discovered value
      val,                // Working boolean value
      stack      = [];    // Stack for nested boolean expressions

  // For each 32 bit integer passed to us as a base 36 string
  for ( let segment of shape ) {

    // Convert to a base 10 integer
    let integer = parseInt(segment, 36);

    // Process each bit in this integer.
    // Note: `while` loop is faster than a `for` loop here.
    let iters = INT_SIZE;
    while (iters--) {

      // Construct our lookahead opcode and "pop" a bit off the end
      // of our integer's binary representation.
      lookahead += integer % 2 * (2 * step || 1);
      integer = integer >>> 1;

      // When we have discovered the next opcode, process.
      if (!(step = ++step % (opcode == 1 ? VAR_SIZE : 3))) {

        // Each opcode type requires implementation
        switch (opcode) {
          // START
          case 0: break;

          // VAL
          case 1: break;

          // NOT
          case 2: break;

          // OR
          case 3: break;

          // AND
          case 4: break;

          // EQUAL
          case 5: break;

          // ---
          case 6: break;

          // CLOSE
          case 7: break;
        }

        // Begin construction of new opcode.
        opcode = lookahead;
        lookahead = null;
      }
    }
  }
}

This example implementation runs at sub-millisecond times (~0.2ms) for even exceptionally long opcode sequences (~2000 bits). The addition of opcode functionality should not increase this significantly as boolean expression shape parsing should be the limiting operation.

Problems

This proposal, as written, does away with the current concept of Source Expressions. All source expressions โ€“ Boolean, Ternary and Switch โ€“ can be encoded for individual classes in their respective boolean expressions. This may not be the most efficient way to encode this logic and there is probably a way to encode these source expressions into the expression shape without increasing opcode size. Certainly something to explore more.

Another source of template bloat comes from having to write all substate values into the template to accommodate dynamic state logic. Substate strings may be best hard coded into a generated file and shared among all templates and instead reference them by UID. However, this opens a whole new can of worms, especially for code splitting. Also, with gzip, the benefits of substate abstraction may be compressed away and is possibly a worthless optimization.

We would be able to get even more template size savings by encoding the Uint32s in base 85, but this would bloat the runtime and slow down base conversion โ€“ may not be worth it.

Add custom linter for return types

This builtin rule will check that there's a declared return type for functions, which I think is overkill. But whenever a function has multiple return statements (or multiple yield statements in a generator), return type should be required.

Possible option: only require a type declaration if the return statements have different types.

This will require a custom linter.

This issue is being filed because it was too complex to fit into #65

Private/Protected Objects?

We should consider whether inheritance needs the notion of a private object. Marking an object as private would make it illegal to resolve with an override.

Protected would be the same but would allow overrides in sub-blocks.

Shared State

I've been wondering if we should have a syntax for creating a state that is shared across all classes.

An example use case for this would be a the gutter and last states for a float-based grid block.

Using Sass, you can create mixins to simplify this process, but I'd like the block syntax to be usable without a preprocessor.

/* example.block.css */
.foo {
   margin: 10px;
}

.bar {
  margin: 18px;
}

*[state|last] {
   margin-right: 0;
}

Would then compile to:

.example__foo { margin: 10px; }
.example__bar { margin: 18px; }
.example__foo--last,
.example__bar--last { margin-right: 0; }

Maybe it's not common enough to warrant addressing with specialized syntax, but I thought I'd write it down for discussion at some point.

Styling Scope State Dependent Descendant DOM

Problem

Consider:
A nav design where a link, containing an icon and a label, are red and blue respectively at rest. When any part of the link is hovered, they both the link and the label should become green:

<nav class="root">
  <a href="#" class="link">
    <svg class="icon"/>
    <span class="label">Label</span>
  </a>
</nav>
.root {
  background-color: black;
}
.link {
  color: red;
}
.link:hover {
  color: green;
}
.icon {
  color: currentColor;
}
.label {
  color: blue;
}

/* This is not currently allowed by css-blocks */
.link:hover .label {
  color: currentColor;
}

Proposals

There are two (2) proposals for how to accommodate for this use case:


Proposal 1: Require developer to make a new block just for the nav link. .link would become that block's .root, and the :hover state on the root would become the new context selector for .label. Blocks allow root states as context selectors and the only change require to make this work is to treat pseudo-selectors on the root class as states.


Proposal 2: Loosen restrictions on Block syntax to allow ClassStates as context selectors, enabling developers to write selectors like .link:hover .label. This syntax relaxation will output the same exact code as creating a new block, but not require the end developer to jump through hoops for simple features like customizing hover effects. Using bare Classes as context selectors would still be disallowed.

By only allowing compound selectors that terminate in a state as context selectors, we still encourage simple, flat style construction, but acknowledge that internal state (read: not root state) may have an effect on other elements within the component.


Proposal 1 upholds the current mental model of css-blocks where root states are the only context selectors allowed by the framework; simplifying development, vastly limiting selector complexity, and preventing CSS specificity foot-guns. Developers are forced to engineer very flat style hierarchies, reducing style complexity and maximizing optimization benefits.

Proposal 2, in most cases, will result in the same exact CSS output as Proposal 1, without the developer overhead of creating a whole new block and wiring up a block-reference just for the sake of creating a very common hover style pattern.

Proposal 2 has the added benefit of enabling very common css patterns that are currently impossible with CSS Blocks, described in more detail below.

Drawbacks

Proposal 1

This issue raises concerns about some exceptionally common CSS patterns that are currently impossible with css-blocks as is. Proposal 1 does nothing to fix these concerns. Consider a custom checkbox component using the only method in CSS for creating styled check boxes:

import checkbox from "./styles.css";

return (
  <my-checkbox class={checkbox}>
    <input type="checkbox" id="my-checkbox" class={checkbox.input} />
    <label for="my-checkbox" class={checkbox.label}>Click me!</label>
  </my-checkbox>
);
// styles.css
.root {
  ...
}
.input {
  opacity: 0;
  pointer-events: none;
}
.label {
  background-color: red;
}
.input:checked + .label {
  background-color: green;
}

The above is not currently possible with css-blocks and no combination of new block files, global states, or inheritance make it work, even after treat pseudo-selectors on the root class as states. (@chriseppstein, please confirm?)

Proposal 2

Proposal 2 has the added benefit of enabling very common css patterns that are currently impossible with CSS Blocks, but with this comes the worry that developers can abuse this behavior, resulting in less-intuitive CSS and impacting optimization. For example, more freely using internal state to style components with sibling selectors instead of using root state:

.an-item[state|active] ~ .following-itmes {
  ...
}

These less-than-ideal selectors can have a non-trivial impact on optimization.

However, the argument can be made that when developers use sibling selectors they do so with intention and disallowing their use can drastically limit the styles we can compose with CSS, as shown above.

Webpack CSS Hot Reloading

Because we take over the css compile / emit step for builds in the webpack-plugin, and future broccoli-plugin, no existing css hot reloading plugins easily integrate with css-blocks. We need to either provide integration documentation, or roll our own css hot reloading.

Rename `state` namespace to `ui`

We have found that the term state is easily confused with the React concept of component state and [ui|statename] is a more concise syntax.

Handle state attribute values in quotes

postcss-selector-parser includes the quotes -- we need to strip them.

> n = selectorParser().process('[state|foo="1"]')
Processor {
  func: [Function: noop],
  res:
   Root {
     spaces: { before: '', after: '' },
     nodes: [ [Object] ],
     type: 'root' } }
> n.res.nodes[0].nodes
[ Attribute {
    operator: '=',
    value: '"1"',
    source: { start: [Object], end: [Object] },
    sourceIndex: 0,
    attribute: 'foo',
    namespace: 'state',
    spaces: { before: '', after: '' },
    type: 'attribute',
    raws: { unquoted: '1' },
    quoted: true,
    parent:
     Selector {
       spaces: [Object],
       nodes: [Circular],
       type: 'selector',
       parent: [Object] } } ]

Improve Style Conflict Error Message

See: #62 (comment)

tl;dr, move template usage data to the error message:

The following property conflicts must be resolved for these co-located Styles:

  template:
    div.class always uses block-a.root (templates/my-template.hbs:10:32)
      and conditionally uses block-b.root (templates/my-template.hbs:10:39)

  color:
    block-a.root (blocks/foo.block.css:3:36)
    block-b.root (blocks/b.block.css:1:30)

  background-color:
    block-a.root (blocks/foo.block.css:3:48)
    block-b.root (blocks/b.block.css:1:43)

Setup TSLint to check for more style preferences

  • indentation rules for class declarations (https://github.com/css-blocks/css-blocks/pull/62/files#r164588816)
  • space inside curly brackets
  • space inside parens
  • space after commas
  • consistent indentation
  • space after function/method declaration
  • space between : and return type.
  • space around binary operators
  • require return type declaration for functions (tbd: only for functions with multiple return statements?)
  • prefer for...of to forEach

and review settings for other whitespace lint rules and make sure we like them and enable them.

Update `addStyle` and `addExclusiveStyle` Interface

Problem

Currently, style correlation methods on Analysis objects expect analyzers to pass the exact BlockObject they wish to add. This forces analyzers to look up block references and pushes a lot of standard error catching logic into the analyzers, complicating analyzer implementation.

Solution

By updating addStyle and addExclusiveStyle to accept the parent Block, and string representations of child BlockObjects to add, block lookup and error throwing can happen in css-blocks core, further simplifying analyzer implementations.

Example

addStyle(block, ".foo", true);
addExclusiveStyle(block, true, ".foo[state|bar]", ".foo[state|baz]");

Issues

Analyzers will have to translate discovered classes and attributes into well-formed selector strings. This string formatting may be unpleasant and result in a lot of boilerplate code. We may consider instead exposing a BlockObject lookup API on Blocks that will throw a helpful error if the block doesn't exist. This removed error checking from the Analyzers, but still provides them a programmatic interface for accessing and passing blocks around.

addStyle(block.getClass("foo", locInfo), true); // Will throw if "foo" is not a class on `block`
addStyle(block.getClass("foo", locInfo).getState("bar"), true); // Will throw if "foo[state|bar]" does not exist on `block`

Runtime: deliver runtime from css-blocks core?

This issue was originally filed by @amiller-gh against the runtime repo which has been merged into the css-blocks monorepo:

It doesn't make much sense to deliver as an independently versioned peer dependency. The rewriters will use a specific version of the runtime and that runtime will most likely be coupled to the version of css-blocks. Instead, css-blocks can:

  1. Return to the rewriters the exact arguments that must be passed into the helper, instead of raw boolean expressions.
  2. Expose the runtime in a well-defined location for rewriters to proxy up to the app they are integrated into.

This will reduce project module complexity and help to unify implementations of css-blocks across the board.

CSS Blocks README Updates

Remove Unimplemented bits from README and into design issues. Review for accuracy. Update with any new concepts and additional details.

default substate

When a block class has a state with several substates, it is convenient to allow one of the substates to be declared as a default.

during rewriting the class(es) for the default substate would get added whenever no other substate for that state is specified.

Shorthand Conflict Resolution

Here's the project that we need to fork: https://github.com/ben-eb/css-values

This is the JSON data that it uses:
https://github.com/ben-eb/css-values/blob/master/data/data.json

Which is a copy and unification of this source data: https://github.com/mdn/data/blob/master/css/

but it has been cleaned up and had a number small issues fixed in the local copy.

According to Ben, this data really ought to be updated against the latest. In theory, there should be a repeatable process for taking updates and applying local patches (which we really need to get back into the MDN official copy eventually -- they accept pull requests).

The source for the code generator is here: https://github.com/ben-eb/css-values/tree/master/src

What we need for css-blocks is to be able to implement the following functions:

function extractLonghand(shorthandProperty: string, value: string, longhandProperty: string): string | null {};

function extractLonghandOrDefault(shorthandProperty: string, value: string, longhandProperty: string): string {};

The first function would extract the value for the longhand from the shorthand or return null if it's not set explicitly.

The second function would always return a value for the longhand, returning the default value for that longhand property if it's not set explicitly.

You can also imagine other useful api's:

  • return all values for the long hands in a given shorthand
  • validate shorthand

Those may be generally useful for other use cases. In general, any support for shorthands should reflect the existing patterns and support for the long hands already in the code.

Keep in mind that some shorthands expand to shorthands that expand to longhands. E.g. border -> border-width, border-style, border-color; border-width -> border-top-width, border-right-width, border-bottom-width, border-left-width.

So it should be possible to request the value for border-bottom-style from a border value.

I'd suggest that we first submit a pull request with the data update.

Then open an issue with proposed api changes and give ben an opportunity to weigh in on the api before we build it.

Compression Ideas

This is a small set of ideas for the compression steps. I'm writing these free-form.

Leverage Brotli Static Dictionary for Key Values
We spoke about this in person, but I'd like to try an experiment with counter hashing leveraging the static dictionary brotli comes with (https://gist.github.com/anonymous/f66f6206afe40bea1f06).

Here's the idea:

  1. Pull all the values from the dictionary and filter out ones invalid for classnames. (eg !, https://gist.github.com/anonymous/f66f6206afe40bea1f06#file-gistfile1-txt-L777)
  2. Use these values for classname keys
  3. Only apply to Brotli compressed version of the assets (this requires separate build of all dependents on a brotli pathway)

CSS Nano Optimizations
Understand you're already likely looking at these optimizations, but wanted to insure it was noted somewhere.

http://cssnano.co/

elision of state

Many states are static within a template and are used to select a display mode for a given component. In these situations we can statically resolve the selectors during optimization to a single unscoped class that can be merged with other declarations at the global level. Note this is only worth doing if all uses of the root-level state are static. if there's a dynamic use in any location, then there's no point in performing the optimization.

To do this, we have to record what classes and states are applied dynamically during template analysis.

it's possible this can be done by the css-blocks compiler by outputting a class that resolves against other classes/states that the specificity would normally resolve or perhaps this can be done purely by the optimizer.

Improve Correlation Data Model Efficiency

Currently, we enumerate and store every possible correlation of BlockObjects. This is expensive in both time and space and can be avoided by instead recording calls to addStyle and addExclusiveStyle, putting off permutation until the latest possible time.

First Reactions

Chris, I'm so so so excited to be working on this with you. So much time has been focused on the JavaScript side of reusable, fast components and CSS remains such a pain point for people (both ergonomically and from a performance perspective).

I wanted to write down my first impressions while I'm still more or less Curse of Knowledge-free. Hopefully documenting the delta between what I was expecting and the initial proposal will be helpful, even if we decide that the first spike is the correct one, because we can better anticipate the objections other people might have.

I think, fundamentally, I was expecting something more "component-oriented." That is probably largely driven by my own biases, having been thinking about React and Glimmer components so much recently. You've been thinking about BEM a lot, so it's no wonder this proposal is framed from that perspective.

I wonder if there's some way we can join these two worlds. The proposal as-is is fairly standalone, in the sense that it introduces its own terminology and concepts. That's a win if the current excitement around components ends up being a fad (two-way bindings, anyone?). But my gut feeling is that it is here to stay, and even folks who don't write a lot of JavaScript will be thinking more in terms of components in the future thanks to Web Components, shadow DOM, etc.

Looking at the form.block.scss example, there's more cognitive overhead around naming than I think is necessary if we can integrate deeply into e.g. the Glimmer component model.

Blocks & Components

If I understand correctly, it seems like there's a pretty natural correlation between a "block" and a "component". It would be nice if we could omit having to declare and name the block and have .scss files co-located with the component implicitly be scoped to the component block.

You still need some way to declare rules for the "component block," and there is some prior art we can consider borrowing. For example, ember-component-css uses the SCSS & parent selector. This could look like:

/* src/ui/components/my-form/style.scss */
& {
  margin: 2em 0;
  padding: 1em 0.5em;

  &:state(compact) {
    margin: 0.5em 0;
    padding: 0.5em 0.5em;
  }
}

We could also theoretically use :component pseudo-selector?

Behind the scenes, this could generate a block with the same name as the component, and we could apply that class to the component automatically.

Auto-Inferring Elements

Can we treat class names in a component template as implicit elements? Using more complex selectors would be a build error unless there was an explicit opt out.

(And can we talk about how confusing re-using the term Element is in BEM?)

Glimmer Example

Spitballing here, but here's an strawman that's integrated into a Glimmer component.

Style

/* src/ui/components/sailfish-form/style.scss */
:component {
  margin: 2em 0;
  padding: 1em 0.5em;
  &:state-set(theme) {
    &:state(red) {
      color: #c00;
    }
    &:state(blue) {
      color: #006;
    }
  }

  &:state(compact) {
    margin: 0.5em 0;
    padding: 0.5em 0.5em;
  }
}

.input-area {
  display: flex;
  margin: 1em 0;
  font-size: 1.5rem;
  :state(compact) & {
    margin: 0.25em 0;
  }
}

.label {
  flex: 1fr;
}

.input {
  flex: 3fr;
  :state(theme red) & {
    border-color: #c00;
  }
  :state(theme blue) & {
    border-color: #006;
  }
}

.submit {
  width: 200px;
  &:substate(disabled) {
    color: gray;
  }
}

Template

{{! src/ui/components/sailfish-form/template.hbs }}
<form class="{{state 'compact'}} {{state 'theme' theme}}">
  <div class="input-area">
    <label class="label">Username</label>
    <input class="input">
  </div>
  <submit class="submit {{state disabled}}">
</form>

Component

// src/ui/components/sailfish-form/component.ts
import Component from '@glimmer/component';

export default class extends Component {
  disabled = true;

  get theme() {
    return this.args.user.isAdmin ? 'red' : 'blue';
  }
}

Output

This is what would be rendered in the final CSS output/to the DOM (non-compressed). (Block name is inferred from component name.)

<form class="sailfish-form sailfish-form--compact sailfish-form--theme-red">
  <div class="sailfish-form__input-area">
    <label class="sailfish-form__label">Username</label>
    <input class="sailfish-form__input">
  </div> 
  <submit class="sailfish-form__submit sailfish-form__submit--disabled">
</form>
.sailfish-form { margin: 2em 0; padding: 1em 0.5em; }
.sailfish-form--theme-red { color: #c00; }
.sailfish-form--theme-blue { color: #006; }
.sailfish-form--compact { margin: 0.5em 0; padding: 0.5em 0.5em; }

.sailfish-form__input-area { display: flex; margin: 1em 0; font-size: 1.5rem; }
.sailfish-form--compact .form__input-area { margin: 0.25em 0; }

.sailfish-form__label { flex: 1fr; }

.sailfish-form__input { flex: 3fr; }
.sailfish-form--theme-red .form__input { border-color: #c00; }
.sailfish-form--theme-blue .form__input { border-color: #006; }

.sailfish-form__submit { width: 200px; }
.sailfish-form__submit--disabled { color: gray; }

Discussion

A few notes about this:

  1. Use :component to target root component block.
  2. It seems like states can either be boolean or string values. I'm not sure how that interacts with the state-set stuff.
  3. The class name for the root component block is auto-generated based on the component name, and added on the template's root element by default.
  4. Any class inside the component's SCSS file is interpreted as an element. That means any combination of class names will throw an error at build time (if I understand elements correctly).
  5. State goes through the {{state}} helper. In this example, it auto-infers the element/block to apply the state/substate to, although I don't know how reliable this would be in practice. Probably easier for the root component block than elements inside. The helper can take either a static value or dynamic data from the template.

WDYT?

Glimmer Template Rewriter

The glimmer template rewriter will use a block-mapping.json file to rewrite classes.

The blockMappings are populated by the block compiler. mappings are recorded by the optimizer.

{
  "blockMappings": [
    {
      "file": "path/to/my-block.block.css",
        "root": {
          "classNames": "my-block",
          "states": {
            "foo": { "classNames": "my-block--foo" },
            "bar": {
              "sub1": { "classNames": "my-block--bar-sub1" },
              "sub2": { "classNames": "my-block--bar-sub2" }
            },
          },
        },
        "classes": {
          "abc": {
            "classNames": "my-block",
            "states": {
            }
          }
        },
        "externals": {
          "classes": [ ],
        }
    },
  ],
  "mappings": {
    "c2": ["my-block--foo", ["asdf", "qwer", "Zxcv"], "my-block--bar-sub2"]
  }
}

blockMappings get from a block root/class/state to one or more css classes.

mappings structure is:

key is the class name that should be applied to the element if the values match.

values is a list of strings or arrays where any of the list entries matches. If the entry is a string then that class must be on the element once block mappings are resolved to classes. If the list entry is an array then all of those values must match.

create bloom filter of all values on right, do a full match if the bloom filter passes.
or maybe create a map of values to array of keys and then do a full match on those keys.

The classname optimizer can transform the classes produced by the block compiler
by replacing the block classNames in both the blockMappings and in the Mappings.

external classes cannot be transformed or mapped they are provided so that they aren't confused for block classes.

Restrict ClassState Styles to Elements With Base Class

Problem

Currently, ClassStates may be applied to elements that do not have their corresponding class styles applied.

// .block__class
.class {
  color: red;
}
 // .block__secondary--filled
.class[state|filled] {
  background: blue;
}
import block from "filename.block.css";
let styles = objstr({
  [block.secondary]: false,
  [block.secondary.filled()]: true
});
<div class={styles}></div> // Receives blue background, no color.

Proposed Solution

In lieu of a runtime, or very complex static analysis, we can slightly modify ClassState output to restrict styles to elements with the corresponding parent class:

// .block__class
.class {
  color: red;
}
 // .block__class.block__class--filled
.class[state|filled] {
  background: blue;
}
import block from "filename.block.css";
let styles = objstr({
  [block.secondary]: false,
  [block.secondary.filled()]: true
});
<div class={styles}></div> // Receives no styling.

This has the additional benefit of maintaining the specificity of pre-transformed block code, avoiding potentially confusing specificity errors.

Potential Drawbacks

This will result in increased pre-optimized file size and may create new considerations for optimization. Discussion needed.

Glimmer Template Analyzer & Validator

Template Analyzer and Validator:

  1. Analyze templates against their corresponding css blocks.
  2. Validate that the classes and states referenced are defined in the block and used legally.
  3. Produce a block-usage.json file:
{
  "template": "path/to/my-component/template.hbs",
  "blocks": {
    "": "path/to/my-component/styles.block.css",
    "ref": "path/to/shared/reference.block.css"
  },
  "stylesFound": [
     "ref.root",
     "ref[state:b]",
     ".root",
     "[state|a]",
     "[state|foo=bar]",
     "ref.link",
     ".foo",
     ".foo[state|asdf]",
     "ref.quux",
     ".class1",
     "ref.ref-class"
  ],
  "styleCorrelations": [
    ["ref.root", "ref[state:b]"],
    [".root", "[state|a]", "[state|foo=bar]", "ref.link"],
    [".foo", ".foo[state|asdf]", "ref.quux"]
  ]
}

Thoughts:

  1. we probably want to produce one block-usage.json per template instead of one big file so that we can only update the data when the templates are changed.
  2. This will use the block parser for validation, but no rewriting occurs at this phase.
  3. This should happen before block compilation, resolution conflicts will be errors from the block compiler and we can use the correlations to guide actual resolutions that are created so that the optimizer has less work to do (because naive resolutions generate more selectors than are strictly necessary for the markup used).

Automatic CSS Variable Scoping

Overview

Currently, CSS variables are treated just like any other property in blocks. This means that conflicting variable names are expected to be explicitly resolved by the developer, but we can do better than that!

Problem

CSS variable can be given special treatment in blocks. Consider two blocks a and b:

/* a.block.css */
:scope {
  --my-var: red;
}
.class {
  color: var(--my-var);
}
/* b.block.css */
:scope {
  --my-var: blue;
}
.class {
  color: var(--my-var);
}

If both blocks are applied to the same element, their --my-var definitions will conflict, causing unexpected behavior and requiring explicit resolution.

Now consider two blocks base and extended:

/* base.block.css */
:scope {
  --theme-color: red;
}
.class {
  color: var(--theme-color);
}
/* extended.block.css */
@block-reference base from "base.block.css";
:scope {
  extends: base;
  --theme-color: blue;
  --local-var: red;
}
.bar {
  color: var(--local-var);
}

Here, the exception is elements with block extended applied will use the re-defined --my-var class for all inherited styles.

Proposal

Compiled blocks should rewrite variable names to unique identifiers. This is easily done by prefixing all vars with their uid:

/* a.block.css */
.a {
  --a-my-var: red;
}
.a__class {
  color: var(--a-my-var);
}
/* b.block.css */
.b {
  --b-my-var: blue;
}
.b__class {
  color: var(--b-my-var);
}

In blocks that extend a base block, conflicting css var names should inherit the name of their base block, while locally defined names should be prefixed with the local uid. So our extended block example becomes:

/* base.block.css */
.base {
  --base-theme-color: red;
}
.base__class {
  color: var(--base-theme-color);
}
/* extended.block.css */
.extended {
  --base-theme-color: blue;
  --extended-local-var: red;
}
.extended__bar {
  color: var(--extended-local-var);
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.