Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-view-transitions-1] CSS selector syntax for generated elements and API names #7788

Closed
jakearchibald opened this issue Sep 24, 2022 · 47 comments

Comments

@jakearchibald
Copy link
Contributor

jakearchibald commented Sep 24, 2022

Folks at TPAC asked for the pseudo-element names to be reduced so we had a bikeshedding session, and here's what we came up with.

@fantasai @mirisuzanne I'm interested to hear your thoughts on this. We've tried to go for brevity rather than full namespacing.

"Shared element transition" becomes "View transition"

Rationale:

  • "shared element" didn't seem accurate. There's no sharing of elements.
  • The DOM changes, and a transition is created from the two states. The actual DOM elements aren't animated, it's just a view of them, so "view transitions".

page-transition-tag becomes
view-name

.header {
  /* old */
  page-transition-tag: header;
  /* new */
  view-name: header;
}

This causes pseudos to be created so the header can be animated independently during the transition.

Rationale:

  • The scope of a transition may not always be the whole "page".
  • Other specs use "name" rather than "tag" to set an identifier (container-name, scroll-timeline-name).
  • Using "view" because "view transition".

html::page-transition becomes
html::view-root

This is the root of the pseudo-element tree that contains all the transition parts.

Rationale:

  • The scope of a transition may not always be the whole "page".
  • Using "view" because "view transition".
  • "root" because it's the root of the pseudo-element tree.

html::page-transition-container(header) becomes
html::view(header)

This is the pseudo-element that represents a view of an element during the transition. In this case it's the view of the 'header'. Developers target this to customize the animation. It's a child of html::view-root.

Rationale:

  • Each element that's given a view-name results in the creation of one of these pseudo-elements. So view-name: header results in a html::view(header) during the transition.

html::page-transition-image-wrapper(header) becomes
html::view-image-group(header)

This wraps the view's images, providing isolation for their blending (to allow a true cross-fade). In this case it's the image-group for the 'header'. It's a child of html::view.

Rationale:

  • The pseudo elements within are replaced elements, but they behave like images. They have a width, height, and an aspect ratio.

html::page-transition-outgoing-image(header) and
html::page-transition-incoming-image(header) become
html::view-before(header) and
html::view-after(header) respectively

These are the replaced elements that represent the before and after view. In their default animation, they cross-fade.

Rationale:

  • Folks had difficulty mapping 'outgoing' and 'incoming'.
  • 'before' and 'after' seem more intuitive, and shorter.
  • The web animation API uses language like "before phase" and "after phase", although this is only in the spec and not exposed in the API.

document.createTransition() becomes
document.createViewTransition()

This is the API developers use to create a transition between states in an SPA.

Rationale:

  • Although it's longer, it removes the ambiguity with CSS transitions.

Example

Taking the slide-from-the-side example from the breakout at TPAC:

Old:

::page-transition-outgoing-image(root) {
  animation: 500ms ease-out both slide-to-left;
}

::page-transition-incoming-image(root) {
  animation: 500ms ease-out both slide-from-right;
}

New:

::view-before(root) {
  animation: 500ms ease-out both slide-to-left;
}

::view-after(root) {
  animation: 500ms ease-out both slide-from-right;
}

(I've omitted the keyframes since they're the same in both)

Resulting pseudo-tree

::view-root
├ ::view(custom-ident)
|  └─ ::view-image-group(custom-ident)
|     ├─ ::view-before(custom-ident)
|     └─ ::view-after(custom-ident)
└ ::view(different-ident)
   └─ ::view-image-group(different-ident)
      ├─ ::view-before(different-ident)
      └─ ::view-after(different-ident)

(thanks @mirisuzanne!)

@jakearchibald jakearchibald added the css-view-transitions-1 View Transitions; Bugs only label Sep 24, 2022
@bramus

This comment was marked as resolved.

@jakearchibald

This comment was marked as resolved.

@ydaniv
Copy link
Contributor

ydaniv commented Sep 25, 2022

View may be confusing since there's already ViewTimeline and its semantics are quite different - closer to those of viewport and viewbox.
And if considering view() it may also be relevant to consider #7587.

I guess a more accurate name would be "Cross Element Transition", but that isn't helping with the brevity part 🙂
Perhaps something like cross-transition? (or x-transition?)

@tbondwilkinson
Copy link

I am quite fond the name "view"!

view-name sounds like it's the name of the view, but really it's a "part" of the overall "view" transition.

Just so I understand, why is there a need to use a new tagging system, rather than reusing one of the existing tagging systems (class, id)? I assume it's hard to support general selectors in psuedoelements here, but still wondering whether with this new DOM structure being represented in pseudoelements, rather you really do need some sort of selector syntax for that. I believe this was proposed elsewhere, I forget what the response was.

html::view(.header)

I do like the shorter names, but naming things just ::view does lose the sense of transition. ::view alone sounds like it could be static, like ::backdrop rather than something ephemeral and specific to a transition. Somewhere in between is ::view-transition

Alternate to before and after is enter and exit, which are slightly shorter.

Rather than view-before is a pseudoclass an option?

::view-transition(root):enter

@jakearchibald
Copy link
Contributor Author

@tbondwilkinson

view-name sounds like it's the name of the view, but really it's a "part" of the overall "view" transition.

view-part-name may be more accurate, but longer.

Just so I understand, why is there a need to use a new tagging system, rather than reusing one of the existing tagging systems (class, id)?

I don't think setting a class or an ID on an element means it should be a seperate part in an animated transition. That isn't what those things semantically mean. And with class especially, it would cause an explosion of layers, since it's extremely common for an element to have a class.

It's common to want to differ the animation based on:

  • Viewport width
  • User preference (eg prefers-reduced-motion)
  • Cascade (like a class name on the root element may influence whether a descendant is a seperate part of an animation)

This is trivial with CSS (@media, the cascade), but really really had to do with attributes.

Besides, this is very much a 'design' feature, so it semantically fits better with CSS than DOM attributes. See also container-name and scroll-timeline-name.

but still wondering whether with this new DOM structure being represented in pseudoelements, rather you really do need some sort of selector syntax for that. I believe this was proposed elsewhere, I forget what the response was.

Yeah, we're working on a few options here. I'll create a separate issue for that. I'm waiting on Mozilla folks to review it, because I want to be sure I'm representing their proposal correctly. That shouldn't impact this naming change though.

Rather than view-before is a pseudoclass an option?

It would work, but I don't see the advantage. This would require ::view-transition(root) to match:

  • the root part
  • the root part's image group
  • the root part's before image
  • the root part's after image

…and I don't see why you'd ever want to do that.

@mirisuzanne
Copy link
Contributor

Bikeshedding overall name can keep happening - short is nice, and clear is also nice, and there's probably a balance worth finding.

I'm also interested in the structure being represented. I know one of the alternatives we discussed at TPAC involved some sort of 'pseudo-element descendant combinators' to make that structure more clear, and I pushed back some on needing a whole new syntax for it. But it's true that the structure gets a bit lost in this variant.

I think that the resulting pseudo-element tree is:

::view-root
├ ::view(custom-ident)
|  └─ ::view-image-group(custom-ident)
|     ├─ ::view-before(custom-ident)
|     └─ ::view-after(custom-ident)
└ ::view(different-ident)
   └─ ::view-image-group(different-ident)
      ├─ ::view-before(different-ident)
      └─ ::view-after(different-ident)

My immediate instinct was to try and put some structure inside the functional notation somehow:

::view-root
├ ::view(custom-ident)
|  └─ ::view(custom-ident image-group)
|     ├─ ::view-image(custom-ident image-before)
|     └─ ::view-image(custom-ident image-after)
...

But looking at it, I'm not convinced that's a good path to take. Maybe the real gain I'm looking for here is just to associate the 'image group' with the 'before' and 'after' images a bit more clearly. Something like:

::view-root
├ ::view(custom-ident)
|  └─ ::view-image-group(custom-ident)
|     ├─ ::view-image-start(custom-ident)
|     └─ ::view-image-end(custom-ident)
...

or:

::view-root
├ ::view(custom-ident)
|  └─ ::view-group(custom-ident)
|     ├─ ::view-start(custom-ident)
|     └─ ::view-end(custom-ident)
...

So in the end, I like the direction you're going - and it maybe does just come down to a bit more bikeshedding. I kinda like start and end, since we already have before and after pseudos that mean something pretty different. But I don't feel strongly.

@tbondwilkinson
Copy link

tbondwilkinson commented Sep 26, 2022

Just so I understand, why is there a need to use a new tagging system, rather than reusing one of the existing tagging systems (class, id)?

Let me rephrase, why not have an opt-in to transitions and then reuse whatever class/id already exists on that element.

<div id="foo"></div>
#foo {
  view-transition: root;
}

Which leads to something like:

::view-transition(root #foo)::enter

@jakearchibald
Copy link
Contributor Author

@mirisuzanne

I'm also interested in the structure being represented. I know one of the alternatives we discussed at TPAC involved some sort of 'pseudo-element descendant combinators' to make that structure more clear

Yeah, I've got a whole other doc on that with all the options that I'd love your opinion on. I'm waiting on @emilio to review it first, because I want to make sure I've represented the shadow DOM pattern correctly & fairly.

I think that the resulting pseudo-element tree is:

::view-root
├ ::view(custom-ident)
|  └─ ::view-image-group(custom-ident)
|     ├─ ::view-before(custom-ident)
|     └─ ::view-after(custom-ident)
└ ::view(different-ident)
   └─ ::view-image-group(different-ident)
      ├─ ::view-before(different-ident)
      └─ ::view-after(different-ident)

Yep, that's correct

I kinda like start and end, since we already have before and after pseudos that mean something pretty different. But I don't feel strongly.

Hmm, start and end feel layout-positional to me (from align-content and co). I get what you mean about ::before and ::after though.

@mirisuzanne
Copy link
Contributor

Hmm, start and end feel layout-positional to me (from align-content and co). I get what you mean about ::before and ::after though.

Yeah, I agree that both have baggage. In my mind the start/end baggage is just a bit more distant, since it's not part of existing pseudo-classes? But it's maybe a small distinction.

@jakearchibald
Copy link
Contributor Author

Both are better than incoming and outgoing anyway

@SebastianZ
Copy link
Contributor

SebastianZ commented Sep 27, 2022

As others mentioned before, naming those pseudo-elements and the API just "view" might be too general and may lead to confusions with other features. Reading the initial renaming proposal, I immadiately thought: Why is "transition" not part of their names anymore when that's the whole point of this feature? And we are defining views that are transitioned, so maybe it should rather be "transition view"?
So my take on that would be to always have transition-view-* as a prefix. I.e.

page-transition-tag becomes view-name

transition-view-name

html::page-transition becomes html::view-root

html::transition-view-root

html::page-transition-container(header) becomes html::view(header)

html::transition-view(header)

html::page-transition-image-wrapper(header) becomes html::view-image-group(header)

html::transition-view-image-group(header)

html::page-transition-outgoing-image(header) and html::page-transition-incoming-image(header) become html::view-before(header) and html::view-after(header) respectively

html::transition-view-before(header)
html::transition-view-after(header)

document.createTransition() becomes document.createViewTransition()

document.createTransitionView()

Note that I am totally aware that this makes the names mostly even longer than the original ones. Though I believe that clarity has higher priority than brevity.

Sebastian

@fantasai
Copy link
Collaborator

Hi @jakearchibald! I love the thought that you put into this. It's maybe obvious, but I'm missing why ::view(header) and ::view-image-group(header) are two different elements? Can we combine them?

I agree with @SebastianZ that we seem to have lost the transition aspect of the names, and maybe that's worth keeping somehow.

If you want to use pseudo-element structure to represent the nesting, maybe something like:

::transition /* root */
::transition::view(header) /* group */
::transition::view(header)::old   /* outgoing image */
::transition::view(header)::new /* incoming  image */

If not, then collapsing down the names:

::transition
::transition-view(header)
::transition-view-old(header)
::transition-view-new(header)

@jakearchibald
Copy link
Contributor Author

jakearchibald commented Sep 28, 2022

@fantasai

It's maybe obvious, but I'm missing why ::view(header) and ::view-image-group(header) are two different elements? Can we combine them?

Right now, every ::view(name) is a sibling in the view root, but we're pretty sure we'll add a feature to allow a ::view(name) to be nested in another ::view(name). We've got some vague docs for that here https://github.com/WICG/shared-element-transitions/blob/main/explainer.md#nested-transition-containers.

In this model, the ::view-image-group(name) provides the blending isolation between the images, so nested :views don't get impacted by the blending.

I agree with @SebastianZ that we seem to have lost the transition aspect of the names, and maybe that's worth keeping somehow.

I'm not against that, but brevity kinda goes out the window. In some cases, like page-transition-tag to view-transition-name, it's getting longer.

If you want to use pseudo-element structure to represent the nesting…

I've got a separate analysis on that, but I'm waiting on @emilio to review it before I share it more widely. I want to make sure I'm representing the shadow DOM idea properly & fairly.

If not, then collapsing down the names:

::transition
::transition-view(header)
::transition-view-old(header)
::transition-view-new(header)

I like old and new for the image names!

Are you ok with ::transition given that CSS transitions already exist, and this feature is unrelated (it uses CSS animations, not transitions)?

@khushalsagar
Copy link
Member

khushalsagar commented Sep 28, 2022

the ::view-image-group(name) provides the blending isolation between the images, so nested :views don't get impacted by the blending.

Clarifying this a bit more. Currently our DOM structure looks like this per tag:

::view(foo)
|_::view-image-group(foo)
   |_::view-old(foo)
   |_::view-new(foo)

And ::view-image-group has the isolation:isolate. If ::view doesn't have any content then ::view-image-group is a no-op and the isolation can directly be added to ::view. But there are a couple of extensions which will make ::view have other content:

  1. Nesting which @jakearchibald mentioned. In that case our DOM structure could look like:

     ::view(foo)
     |_::view-image-group(foo)
     |  |_::view-old(foo)
     |  |_::view-new(foo)
     ::view(bar)
         |_::view-image-group(bar)
            |_::view-old(bar)
            |_::view-new(bar)
    
  2. Content capture where we want the snapshot that goes into old/new to be limited to the element's descendants. Box decorations (like background) are kept as styles which are copied over to ::view so you effectively get the same rendering as the element. Except in this mode you can interpolate those styles instead of a cross-fade with those baked into the snapshot which happens right now. In that case ::view(foo) would have a background and blending the 2 images should happen before drawing the result into this background.

I'm good with renaming ::view-root to include the word transition so its obvious that it's related to this feature. Maybe ::view-transition-root then. Depending on the syntax we can avoid adding transition for all the new pseudos but sounds like for any property which appears outside the nested syntax we do want the word transition there?

@astearns
Copy link
Member

transition is long and the feature actually uses animations. animation is also long. view by itself doesn’t say what is being done to the view. What about view-swap which is short, easy to spell, and says what is being done (but is vague on the details of how)

@fantasai
Copy link
Collaborator

fantasai commented Oct 4, 2022

Just throwing out some ideas...

Using "swap" instead of "transition" as @astearns suggested and adding an image-pair wrapper to support nesting going forward...

::swap
::swap-view(header)
::swap-view-group(header)
::swap-view-old(header)
::swap-view-new(header)

or maybe

::view-swap
::view-swap-group(header)
::view-swap-set(header)
::view-swap-old(header)
::view-swap-new(header)

(I don't quite like the confusion over whether groups or sets are outermost.)

I think View Transitions is a very natural name for this feature, though, and keeping it in the pseudo names is long but also helpful?

@khushalsagar
Copy link
Member

I don't quite like the confusion over whether groups or sets are outermost.

Would ::swap-view-images or ::view-swap-images make that clearer? Since the only nodes underneath it would be the old and new images. The flip side is, it can be odd for a single node to have plural images in the name.

I think View Transitions is a very natural name for this feature, though, and keeping it in the pseudo names is long but also helpful?

Between transition and swap, I like transition better too. It's not using CSS transitions but it's conceptually doing something similar.

@jakearchibald
Copy link
Contributor Author

(on holiday, so sorry for the brief comment)

Yeah, "swap" to me sounds like the opposite of a transition. As "the visual state just swaps, rather than transitions".

@jakearchibald
Copy link
Contributor Author

@fantasai

I think View Transitions is a very natural name for this feature, though, and keeping it in the pseudo names is long but also helpful?

Yeah, I'm thinking:

view-transition-name:

::view-transition-root
::view-transition(name)
::view-transition-images(name)
::view-transition-old(name)
::view-transition-new(name)

document.createViewTransition();

It doesn't do much for brevity, but I think it's pretty descriptive.

I've gone with ::view-transition(header) for brevity, since it'll be more common to select that than the root, which I've given ::view-transition-root.

@fantasai
Copy link
Collaborator

fantasai commented Oct 5, 2022

@jakearchibald That looks pretty self-consistent! A couple minor things I'd modify:

  • We cold drop -root, and just have ::view-transition without parens be the root
  • I think I like ::view-transition-set better than ::view-transition-images

So

view-transition-name:

::view-transition
::view-transition(name)
::view-transition-set(name)
::view-transition-old(name)
::view-transition-new(name)

document.createViewTransition();

How well does this structure work with your nesting proposal?

@khushalsagar
Copy link
Member

We cold drop -root, and just have ::view-transition without parens be the root

That makes things ambiguous. Especially because we do want the function syntax to allow selecting all pseudo-elements for a particular type. For example:

::view-transition
::view-transition(*)
::view-transition-set(*)
::view-transition-old(*)
::view-transition-new(*)

The fact that ::view-transition(*) and ::view-transition selects different elements is not intuitive. ::view-transition-root is longer but better in this regard.

document.createViewTransition();

See related discussion in #7828, is document.transition() or document.transitionView() better to make it obvious that it also starts the transition (like element.animate()).

How well does this structure work with your nesting proposal?

Nesting could allow us to drop the transition word since it would be obvious in the syntax:

::view-transition-root
::view-transition-root::view(name)
::view-transition-root::view(name)::view-set
::view-transition-root::view(name)::view-set::view-old
::view-transition-root::view(name)::view-set::view-new

But the css function syntax in the first one seems much simpler to directly target these elements. Do you think it's better to just go with those? If we do resolve on backing these with a pseudo-element tree (instead of shadow DOM) then the nested syntax will work naturally (similar to other pseudo-elements) but with the longer name. I think that's fine since developers will likely use the function syntax.

::view-transition-root::view-transition(name)::view-transition-set::view-transition-old

@astearns
Copy link
Member

astearns commented Oct 5, 2022

Yeah, "swap" to me sounds like the opposite of a transition. As "the visual state just swaps, rather than transitions".

That’s fair. I am just throwing out shorter, vaguer options to test out the theory that view-transition is the best we can do. What about view-change?

@fantasai
Copy link
Collaborator

fantasai commented Oct 5, 2022

Nesting could allow us to drop the transition word since it would be obvious in the syntax:

I was referring to the proposal described in #7788 (comment) not actually structuring the pseudo-elements.

If we're going to use pseudo-element structure, we should drop a lot more of the redundancies in the pseudo-element names, since they'll be scoped by their parent.

E.g.

::transition
::transition::view(name)
::transition::view(name)::set
::transition::view(name)::set::old
::transition::view(name)::set::new

The main concern I have is how this all plays with the nested transitions extension.

@khushalsagar
Copy link
Member

The main concern I have is how this all plays with the nested transitions extension.

The nested transition extension shouldn't be an issue with this. We have 2 selector options, the chaining pseudo-element syntax that you mentioned above. With nested transition, the chaining syntax would look something like:

::transition::view(name)::view(name-child)::view(name-granchild)::set::old

Would it be fair to converge on a name with the assumption that a redundant prefix will be present depending on whether we shorter CSS functions or only the long pseudo-element chaining selector. Your comment above captures the chaining version. Same thing but with CSS function that has an extra "transition-" at the beginning:

html::transition
html::transition-view(name)
html::transition-set(name)
html::transition-old(name)
html::transition-new(name)

With nested transitions extension, html::transition-view(name) selects the corresponding view no matter where it is in the pseudo-element hierarchy.

s/transition/swap or s/transition/change in the above for Alan's name options. I'm still biased towards transitions. Partly because that's the keyword used by the same functionality in JS frameworks or native platforms.

@css-meeting-bot
Copy link
Member

css-meeting-bot commented Oct 5, 2022

The CSS Working Group just discussed [css-shared-element-transitions-1] Renaming and brevity, and agreed to the following:

  • RESOLVED: Prop: Use css-view-transitions as the shortname
The full IRC log of that discussion <dael> Topic: [css-shared-element-transitions-1] Renaming and brevity
<dael> github: https://github.com//issues/7788
<dael> vmpstr: I can intro. We want to rename structure to something more meaningful
<dael> vmpstr: I would hope can resolve on feature name so can move to FPWD with a URL that's fixed
<dael> vmpstr: JakeA proposed view-transition prefix to most things.
<dael> vmpstr: Has been activity today discussing details
<dael> astearns: Note we can pick a fixed URL and then change syntax. We've done it before. Shouldn't look at shurt URL as unchangable thing.
<dael> fantasai: But shared-element-transitions is not the name we want. Do we use view-transitions or css-view-transitions as the URL short name?
<khush> +1 for css-view-transitions
<dael> florian: Seems reasonable. might want ot change later, but not clear we will.
<dael> astearns: Anyone argue against view-transitions?
<dael> astearns: Could resolve on css-view-transitions as the short spec name. Can also resolve to change all draft syntax.
<TabAtkins> Find with the shortname. Still not particularly happy with the syntax options, yeah
<dael> fantasai: Can we do shortname first before we dive to everything else?
<dael> astearns: Prop: Use css-view-transitions as the shortname
<dael> astearns: Obj?
<dael> RESOLVED: Prop: Use css-view-transitions as the shortname
<dael> khush: Limit discussion to spec name or also touch on names for pseudo elements? Got a good bunch of options there
<dael> astearns: Are you looking to have the discussion on call or continue on issue when there has been a little more async discussion?
<dael> vmpstr: If we can timebox discussion? Want a sense of uncertainty. I think JakeA prop names are pretty reasonable. I feel like close to resolution
<dael> astearns: Let's spend 10 min
<vmpstr> https://github.com//issues/7788#issuecomment-1266772511
<dael> vmpstr: In Jake's prop ^
<dael> vmpstr: All the pseudo elements have view-transition prefix. The property to tag elements with an id is view-transition-name. Pseudos are view-transition-root. Images old and new are subpseudo elements.
<TabAtkins> I prefer -images fwiw. The "set" doesn't mean anything afaict
<dael> vmpstr: fantasai prop view-transition-set whic makes sense. Also prop to drop root from view-transition-root but that got pushback
<dael> astearns: images vs set. Let's discuss
<dael> astearns: fantasai can you say why good to change to set?
<khush> i'm ok with set. it's smaller. :)
<dael> fantasai: It's kind represent 2 images that correspond. It's not a selector that represents each image but represents the set that correspond. Captures it's a container
<TabAtkins> q+
<dael> astearns: If this is just [missed]. If you can duplicate on old and new and get same result why set?
<dael> fantasai: Theres a wrapper for ar eason. If we were being really verbose with image-wrapper we could, but I think I prefer succinct. I like using transition-set on the wraper for 2 images
<astearns> ack TabAtkins
<dael> vmpstr: It's (image) also singular which makes it weird so set is good
<dael> TabAtkins: I like images, the plural makes it clear that it's a set. I find set ot be so content-less I have no clue what it means. This structure can be various levels. Don't know why call one a set. I vote images
<vmpstr> we also discussed -image-group
<dael> fantasai: Other thing about images is we're getting into, I don't know, the fact that it is an image we should knwo that but what we're representing is a snapshot of older and newer version
<dael> fantasai: Feels different than an image that is a replaced element
<dael> TabAtkins: The fact that it's an image in browser is important
<dael> fantasai: Images pseudo-element is not an image, it's a wrapper around 2. Since old and new doesn't have work 'image' having 'image' in wrapper is confusing
<dael> astearns: Agree set is vague and images isn't great since it's a plural. Can someone describe what this is used for?
<dael> vmpstr: It's a wrapper that sets up isolation for blending of 2 images. Not a replaced element, but container of 2 images being blended together.
<dael> astearns: What are you going to use this pseudo element for?
<dael> vmpstr: Not sure I understand. The opacity and blend modes of images will interact in it without effecting other things b/c it's isolated
<dael> fantasai: I think astearns is asking how an author is likely to use this pseudo element
<iank_> its similar to various cross-fade effects
<dael> vmpstr: I see. I don't think there's a particularly good guidance on how to use. Typically dev wants to customize container above this, the parent of wrapper, or the opacity blend of images.
<dael> vmpstr: Are some use cases where dev would want to shift container up or down. It moves left to right and container rises from below
<iank_> effect-group?
<dael> khush: Saw a demo where someone added a small animation to give a pop effect. Not what we anticipated for usage. We did it for cross fade, but that's a way we've seen devs using it.
<dael> astearns: Given this is something that we don't have explicit use cases but we know people will use it. not crucial, though. Could go a longer name like view-transition-effect-group or -image-set
<dael> fantasai: If you're going for long view-transition-old-new-set. I think it's good to not use image b/c not really an image
<TabAtkins> (it is really an image)
<fantasai> sorry, I meant conceptually
<fantasai> it is implemented as an image
<TabAtkins> I mean conceptually too
<dael> khush: one of the earlier sugegstions was view-transition-group. Is that a good compromise? Maybe view is better than set.
<dael> astearns: TabAtkins bjeciton to group?
<dael> TabAtkins: It's jsut as generic and non-meaninful. If it's not high use I would say make it longer with a name for its purpose.
<dael> TabAtkins: Where we expect style have shorter names like old new
<dael> astearns: I suggest we take this back to the issue. Let's continue with this open and come back after a bit more discussion.
<dael> astearns: I was going to suggest breaking it out, but there's good discussion so continue there
<fantasai>khush, I think wrt group, since in e.g. SVG and vector editing, "groups" are a hierarchical concept, so seems more appropriate for the higher-level grouping construct that you can nest other things inside rather than for the image-pair-wrapper

@khushalsagar khushalsagar changed the title [css-shared-element-transitions-1] Renaming and brevity [css-view-transitions-1] CSS selector syntax for generated elements and API names Oct 20, 2022
@vmpstr
Copy link
Member

vmpstr commented Oct 21, 2022

I continue to object to the idea that calling the images "images" is somehow confusing.

The plurality of the name is confusing to me. This is referring to a single element, which indeed is a container of images, but it's a container (singular), it's not images. When I'm selecting this element, and I have view-transition-images(...) { ... } I know that this is a single element, because I worked on this feature :) but if I didn't, I don't know if this is some sort of magic CSS to select all constituent images.

To draw a contrived analogy, it's kind of like instead of <ol> calling it <ordereditems>. It isn't items, it's a list (singular) of items and when selecting ol { } i'm selecting a single container element vs ordereditems { } selecting seemingly multiple items

Personally, I don't have any objections to using the word "image", but I want this to be something like image-group or image-list, not images

@vmpstr
Copy link
Member

vmpstr commented Oct 21, 2022

In the interest of summarizing this for a in-person discussion, below is the list of elements that we need to have a name for:

  • view transition root, the top candidate seems to be ::view-transition
  • the container element, which is the child of the root and has size/transform of the underlying element on it. The top contender is ::view-transition-container
  • The wrapper element, which has isolation and contains in it one or two images. This doesn't have consensus, with the most of the discussion being about this element
  • the outgoing image from the "old" state. Top choice: ::view-transition-old
  • the incoming image from the "new" state. Top choice: ::view-transition-new
  • the function to initiate the transition, currently createDocumentTransition. Since this is also now specced to begin the transition, I prefer a verb name with the proposal: document.transitionView() read as a verb, not a noun, which is analogous to element.animate().

Also, we don't have an agreement on the selector syntax
Option 1 This should be chained like:
html::view-transition::view-transition-container(foo)::view-transition-some-wrapper(foo)::view-transition-old(foo)
Option 2 presumably we can drop "foo" and "view-transition" prefix from nested elements:
html::view-transition::container(foo)::some-wrapper::old

Option 3 or do we select this as if they are all children of html:
html::view-transition, html::view-transition-old(foo), etc

Option 4 or do we have part-like selector function that just "figures it out":
::view-transition-part(container foo), ::view-transition-part(old foo)

The last option is also related to #7928

My proposal is to chain this to do option 1 and option 4. Chain it like this:
html::view-transition::view-transition-container(foo)::view-transition-some-wrapper(foo)::view-transition-old(foo)
and also provide a convenience part-like function:
view-transition-part(container foo)

@khushalsagar
Copy link
Member

khushalsagar commented Oct 25, 2022

Thanks for summarizing this Vlad! One thing missing from the list above is the CSS property used to tag a DOM element. I think we have consensus there on view-transition-name.

I'll post a combination of name + selector syntax from the options above for each pseudo-element. So we can have a precise resolution:

Option 1 (Full chaining)

/* root for all pseudos */
html::view-transition
/* container for DOM elements with tag foo */
html::view-transition::view-transition-container(foo)
/* Isolation node for the 2 replaced elements */
html::view-transition::view-transition-container(foo)::view-transition-image-group(foo)
/* Replaced element for old content */
html::view-transition::view-transition-container(foo)::view-transition-image-group(foo)::view-transition-old(foo)
/* Replaced element for new content */
html::view-transition::view-transition-container(foo)::view-transition-image-group(foo)::view-tranition-new(foo)

/* Each selector with args uses * to select all pseudo-elements of that type */
html::view-transition::view-transition-container(*)
html::view-transition::view-transition-container(*)::view-transition-image-group(*)
html::view-transition::view-transition-container(*)::view-transition-image-group(*)::view-transition-old(*)
html::view-transition::view-transition-container(*)::view-transition-image-group(*)::view-transition-new(*)

Option 2 (Reduced chaining)

/* root for all pseudos */
html::view-transition
/* container for DOM elements with tag foo */
html::view-transition::container(foo)
/* Isolation node for the 2 replaced elements */
html::view-transition::container(foo)::image-group
/* Replaced element for old content */
html::view-transition::container(foo)::image-group::old
/* Replaced element for new content */
html::view-transition::container(foo)::image-group::new

/* Each selector with args uses * to select all pseudo-elements of that type */
html::view-transition::container(*)
html::view-transition::container(*)::image-group
html::view-transition::container(*)::image-group::old
html::view-transition::container(*)::image-group::new

This option is terse but we'd end up using old and new as pseudo-element names which seem too generic.

Option 3 (Direct html selector)

/* root for all pseudos */
html::view-transition
/* container for DOM elements with tag foo */
html::view-transition-container(foo)
/* Isolation node for the 2 replaced elements */
html::view-transition-image-group(foo)
/* Replaced element for old content */
html::view-transition-old(foo)
/* Replaced element for new content */
html::view-transition-new(foo)

/* Each selector with args uses * to select all pseudo-elements of that type */
html::view-transition-container(*)
html::view-transition-image-group(*)
html::view-transition-old(*)
html::view-transition-new(*)

Option 4 (Part like)

/* root for all pseudos */
html::view-transition-part(root)
/* container for DOM elements with tag foo */
html::view-transition-part(container foo)
/* Isolation node for the 2 replaced elements */
html::view-transition-part(image-group foo)
/* Replaced element for old content */
html::view-transition-part(old foo)
/* Replaced element for new content */
html::view-transition-part(new foo)

/* Specifying the part associated with only the element type selects all elements of that type */
html::view-transition-part(container)
html::view-transition-part(image-group)
html::view-transition-part(old)
html::view-transition-part(new)

@fantasai
Copy link
Collaborator

fantasai commented Oct 26, 2022

A few bikeshedding suggestions:

  • changing ::view-transition-image-group to html::view-transition-pair because it's shorter (and not a hyphenated phrase) and avoids evoking the idea of groups in SVG, which it's not conceptually similar to
  • changing ::view-transition-container to ::view-transition-group because it's shorter and evokes the idea of groups in SVG (which create hierarchy in the graphic elements)

@jakearchibald

This comment was marked as resolved.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed Selector syntax for generated elements, and agreed to the following:

  • RESOLVED: Go with the Option 3 syntax.
  • RESOLVED: Name the function .startViewTransition()
The full IRC log of that discussion <TabAtkins> Topic: Selector syntax for generated elements
<TabAtkins> github: https://github.com//issues/7788
<TabAtkins> vmpstr: We've discussed the names previously
<TabAtkins> vmpstr: Two parts - what names do the pseudos have, and how to select them in CSS?
<TabAtkins> vmpstr: Summarized in one of the later comments
<vmpstr> https://github.com//issues/7788#issuecomment-1287309961
<TabAtkins> vmpstr: some names that don't seem to have much discussion
<TabAtkins> vmpstr: A lot focused on "images" vs "group/set/etc" for the wrapper element that has isolation on it
<TabAtkins> vmpstr: don't think we have consensus
<TabAtkins> vmpstr: But hoping we can resolve on the rest
<TabAtkins> vmpstr: So, about selection, option 1 is full chaining
<TabAtkins> vmpstr: Shows the whole structure of the pseudos
<TabAtkins> vmpstr: option 2 is the same but with repetition removed from the nested names
<TabAtkins> vmpstr: option 3 is we let all the elements hang off of the root, so you can select any of them directly without needing to navigate the nesting
<TabAtkins> vmpstr: option4 is ::transition-part() which is similar syntactically to ::part() but otherwise resembles option 3
<astearns> ack fantasai
<TabAtkins> fantasai: I don't think the redundant chaining is a good idea, should eliminate it.
<JakeA> q+
<TabAtkins> fantasai: Either reduced chaining or direct attachment makes the most sense
<TabAtkins> fantasai: I think (1) is unnecessarily terrible, 2 and 3 seem like best options
<TabAtkins> fantasai: No strong opinion between those two
<TabAtkins> fantasai: Slightly concerned in 2 when you add the subtree version of the transitions, will it make these longer or keep the same?
<TabAtkins> fantasai: I think there was discussion about nesting inside of things, so if we reflect the full structure will it get longer?
<TabAtkins> flackr: i don't think it will
<TabAtkins> vmpstr: I think it will, if we do the full chaining
<TabAtkins> khush: Yes, same as existing pseudos, you have to have the full chain (like ::before::marker isn't matched by ::marker)
<miriam> q+
<TabAtkins> [missed a clarification]
<TabAtkins> fantasai: So I think that would be a reason to not go with 2
<TabAtkins> vmpstr: So that leaves 3 as your pref?
<TabAtkins> fantasai: Based on what I know so far, yes.
<astearns> ack JakeA
<TabAtkins> I also vote 3, fwiw
<TabAtkins> JakeA: There was a proposal for a pseudo-descendant combinator which would let you skip some of the things in option 1 and 2
<vmpstr> :>> old
<smfr> q+
<TabAtkins> JakeA: But with option 2 there's a worry there's an ambiguity - a short `::old` which is nicely short and contextual, suddenly becomes potentially clashing with other uses
<astearns> ack miriam
<astearns> q+
<TabAtkins> miriam: I asked in the doc - on 4, is it possible to allow combinators within the function?
<TabAtkins> miriam: i'm thinking about, "when you have old but not new, do something special" - can we attach that to a part-like syntax?
<TabAtkins> astearns: Would that just come after the function?
<TabAtkins> miriam: I was imagining it inside but mayb eit does work
<TabAtkins> khush: Since this borrows the ::part() syntax, the keywords that go inside it are like class names.
<JakeA> q+
<astearns> ack smfr
<TabAtkins> khush: we could add a new tag marking when something has just-old, just-new, or both, but you wouldn't be able to query arbitrary things yourself
<astearns> ack astearns
<TabAtkins> smfr: Can I use normal descendant selectors to address these elements?
<TabAtkins> JakeA: No, not true for pseudos in general
<vmpstr> TabAtkins: you can't use general descendant selectors into shadows
<TabAtkins> smfr: seems bad to lose the ability to use familiar selector syntax
<TabAtkins> TabAtkins: true for shadows too - all you ahve is ::part(), which loses structure
<TabAtkins> smfr: can you write a selector that does do structure inside the shadow?
<TabAtkins> TabAtkins: no
<TabAtkins> smfr: unfortunate. Seems the nested double-colon things are unwieldy.
<TabAtkins> fantasai: These things we're selecting, are they elements or are they pseudos?
<TabAtkins> fantasai: Can you pull them out and change their styles with JS?
<TabAtkins> JakeA: Per previous resolution, they're pseudo-elements.
<fantasai> html::view-transition(:root)
<TabAtkins> fantasai: What about something like option 4 but rather than part-like with keywords, it takes a selector subset?
<fantasai> html::view-transition(old)
<fantasai> html::view-transition(.currenttransitionthingIlike new)
<fantasai> html::view-transition(.currenttransitionthingIlike > new)
<smfr> q+
<miriam> +1 that's what I was trying to get at
<TabAtkins> fantasai: So we'd take some subset of selectors, you can use the tree-querying selectors like :root or :first-child
<JakeA> q-
<TabAtkins> khush: So ::view-transition just gets you at the root node for these pseudos, then any selector that you can use today will traverse this tree?
<TabAtkins> fantasai: Pretty much, yeah.
<miriam> https://docs.google.com/document/d/1kW4maYe-Zqi8MIkuzvXraIkfx3XF-9hkKDXYWoxzQFA/edit?disco=AAAAiKGTYoY
<TabAtkins> JakeA: In this model where would I be saying I"m targeting the "new" of the header?
<TabAtkins> ::view-transition(images.header > old)
<fantasai> html::vt(.header new) /* if flat */
<fantasai> html::vt(.header > image-set > new) /* if complicated */
<TabAtkins> JakeA: a pseudo with a class name seems different but i don't hate it...
<TabAtkins> (I think this is a good idea actually.)
<astearns> ack smfr
<astearns> ack fantasai
<Zakim> fantasai, you wanted to ask about subtree selectors
<JakeA> I like that it retains the structure
<ydaniv> or ::view-transition(::new) ?
<TabAtkins> smfr: The things being styled are the snapshots, right?
<TabAtkins> vmpstr: yes
<fantasai> fantasai^: I don't know if it's a good idea, but it's an idea
<TabAtkins> smfr: Would it be possible to describe these by pointing to the element being snapshotted?
<TabAtkins> JakeA: I tried to create a proposal based on that, but in many cases you're animating somethign that's no longer there
<TabAtkins> JakeA: So trying to target somethign that might be gone (or which the matched els have changed) is weird
<TabAtkins> smfr: Seems like you'd snapshot
<TabAtkins> JakeA: IN a multi-page transition you'd be mixing styles from the old and new page, whichi gets tricky.
<TabAtkins> JakeA: The model is for multi-page the incoming page gets to control the animation
<emilio> q+
<TabAtkins> smfr: Yeah I see that case is hard
<TabAtkins> smfr: Is multi-page sitll a realistic goal?
<TabAtkins> JakeA: Yeah, I'm working on it right now
<TabAtkins> smfr: Still seems the from and to page have to have a close enough appearance, so maybe it's not too bad to require them to have close structures
<vmpstr> q?
<TabAtkins> flackr: The containers around the snapshot don't belong to either starting or ending element
<astearns> ack emilio
<TabAtkins> JakeA: I think smfr was suggesting a pseudo targeting that
<TabAtkins> [missed what that actually meant]
<TabAtkins> emilio: Is the idea that the pseudos are still on the root?
<TabAtkins> vmpstr: Yes, if you tag elements in your page they get flattened to a list attached to the root
<TabAtkins> vmpstr: In the later proposal instead of flattening we'd retain the hierarchy from the DOM, but the structure would still attach to the root element
<TabAtkins> emilio: So these pseudos will still only be usable from the root
<TabAtkins> vmpstr: yes
<TabAtkins> emilio: I"m not a fan of "arbitrary selectors", mostly becuase we've already seen it's weird that we need extra containers for wrappers and isolation
<TabAtkins> emilio: So once we expose the structure we can't change that ever
<TabAtkins> emilio: maybe that's ok
<JakeA> q+
<TabAtkins> TabAtkins: Several of the syntaxes already epxose the structure, and I think in general styling will expose the structure.
<TabAtkins> TabAtkins: Don't think the structure can be changed without a mode switch
<TabAtkins> emilio: say you have a new transition type that needs a new wrapper
<TabAtkins> emilio: stuff generally keeps working
<fantasai> +1 to emilio's point
<TabAtkins> khush: I was expecting one thing that would be consistent is that the only thing that affects the structure is view-transition-name
<TabAtkins> khush: So if you ahve two doms and have the same tag applied, you'll consistnetly get the same structure
<TabAtkins> khush: So if the structure changes I expect it's from the author changing something
<TabAtkins> khush: so was emilio talkinga bout changing the structure even if the authors don't change anything?
<TabAtkins> emilio: Yeah, if you want to expose a new transition type with a differetn structure
<TabAtkins> q+ about this
<TabAtkins> ack about
<TabAtkins> ack this
<TabAtkins> q+
<astearns> ack JakeA
<TabAtkins> JakeA: The reason we added the image wrapper was to give us some leeway for this nested model
<TabAtkins> JakeA: otherwise we could use the container itself for the isolation
<TabAtkins> JakeA: We didn't think we could add structure later without breaking author styles
<TabAtkins> JakeA: There's also Miriam's point about "only old" or "only new" wanting a different animation
<TabAtkins> JakeA: So if we use :only-child for that that does lock us into some structure
<vmpstr> TabAtkins:
<astearns> ack TabAtkins
<fantasai> scribe+
<fantasai> TabAtkins: Concern that emilio has, not necessarily that we change structure of existing transitions, but if we add a new type that needs a new structure
<fantasai> TabAtkins: concern would be if you have styles generically applying to old styles, and add new transition type, something might break
<fantasai> TabAtkins: don't think we should be concerned; if you make a new type of transition, make new styles
<fantasai> TabAtkins: also not sure any existing styling would work properly under a new structure, might implicitly depend on it
<fantasai> TabAtkins: so not guaranteed to work. And it's a new feature anyway
<fantasai> TabAtkins: so I don't think this is a very strong argument
<astearns> ack fantasai
<khush> q?
<JakeA> q+
<TabAtkins> fantasai: say, picking out the old from a transition is a little tricky
<TabAtkins> fantasai: if you want the old header transition, you ahve to pipe thru the whole structure and use child selectors
<TabAtkins> fantasai: if the header has a nested icon also transitioning (assuming nested transitions) you can't just generically address olds
<TabAtkins> fantasai: So using the selector syntax has more power but does require a little extra work
<khush> q+
<TabAtkins> fantasai: So my inclination is option 3, and if at some point we need to add more structure we can do 4 with selectors
<fremy> +1, I think this is the type of things we can add later
<TabAtkins> fantasai: But I think ahving a pseudo that doe sthe lookup cleanly seems fine anyway
<astearns> zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<astearns> ack khush
<argyle> q+
<astearns> ack JakeA
<TabAtkins> TabAtkins: I was assuming that everything under a tag would be tagged with it, so `old.header` would work instead of needing `.header > images > old`
<TabAtkins> khush: +1 to option 3, dealing with selectors would just be hard
<TabAtkins> khush: Also yes, we do tag every element with the given tag
<TabAtkins> JakeA: We should capture the selector idea in the issue for later, tho
<TabAtkins> argyle: Somethign I like about 4 is it's clear what's being selected
<TabAtkins> argyle: Since 3 is nice, I suggest instead of using a pseudo-element function we just use a descendant combinator
<fantasai> I think argyle is asking for something like html::view-transition old.header
<fantasai> ?
<fremy> @argyle: do you mean `::view-transition #id` ?
<TabAtkins> yeah adam is asking for elika's idea, just not inside the parens
<TabAtkins> vmpstr: The "foo" in option 3 is the tag of the container itself, it's not like :has()
<JakeA> seems ambiguous with descendant combinators?
<fremy> JakeA: yeah, I think the idea is that it's like a descendant in the "view-transition" tree
<TabAtkins> argyle: Okay, I was misunderstadning how the targeting worked, the pseudo-element is targeting the pseudo in question
<fremy> fantasai, ah yes, good idea. I'm falling for the autocomplete...
<Sebastian_Zartner> +1
<TabAtkins> astearns: So proposal is to go with option 3, leaving the possibility of future selector power
<TabAtkins> RESOLVED: Go with the Option 3 syntax.
<Sebastian_Zartner> +1 on option 3 with option 4 for future extensions
<TabAtkins> vmpstr: are the names part of this resolution, or do we need to discuss those separately?
<TabAtkins> astearns: This is the CSSWG, we'll definitely discuss names again. But go with what's in the issue for now.
<TabAtkins> vmpstr: Also the JS api name to start a transition isn't good.
<TabAtkins> vmpstr: Proposal is .transitionView()
<vmpstr> document.transitionView()
<TabAtkins> khush: Took inspriation from element.animate()
<TabAtkins> astearns: Is there an issue open for this?
<TabAtkins> vmpstr: it's part of this issue
<TabAtkins> smfr: "transition view" reads like a noun
<TabAtkins> smfr: startViewTransition()?
<fantasai> +1 to startTransitionView
<TabAtkins> JakeA: createViewTransition() was what we had before
<fantasai> or startViewTransition
<TabAtkins> ydaniv: There's a separate issue about whether we should start by default
<TabAtkins> astearns: Objections to .startViewTransition() ?
<TabAtkins> RESOLVED: Name the function .startViewTransition()

@jakearchibald
Copy link
Contributor Author

@fantasai and @mirisuzanne suggested a syntax like this:

html::view-transition(.header new) {
  …
}

Where the bit within view-transition() would allow for a complex selector where the view-transition-name is 'turned into' a class name, and the parts of the structure are 'turned into' tag names. So the above is selecting 'new' parts within the container with view-transition-name of header.

@FremyCompany
Copy link
Contributor

FremyCompany commented Oct 26, 2022

Note that if we want to allow complex selectors like this in the future, but not in the first version, this is possible if we change the syntax to allow ID selectors only:

html::view-transition(#group-foo) {
  …
}
html::view-transition(#old-foo) {
  …
}
html::view-transition(#new-foo) {
  …
}

(where the ids are auto-generated based on the syntax that would be used in proposal 4)

That enable us to go for more complex selectors later, if there is a demand, without adding much difficulty for authors (only add one pound symbol in the function).

Of course, if we are quite sure we won't need complex selectors, this is moot. Just a random thought.

@khushalsagar
Copy link
Member

Filed #7960 since we didn't get to resolve on all the names today.

@jakearchibald regarding the syntax you mentioned, when you say tag names are you drawing a parallel with element tag like img. So the proposal there would be equivalent to a selector like this in CSS:

.class-foo img {
  ...
}

I assumed this proposal is about allowing existing selector patterns as the function argument but the above is not valid syntax today. Maybe I misunderstood the proposal.

@FremyCompany that's an interesting thought. ID is not a good concept though since it'll be unique per pseudo-element and we do want to allow selecting all containers for example. Classes is better. But I'm a little wary of supporting all complex selectors there.

The use-cases for more complex selectors we've seen have been about rules which are conditional based on the DOM structure. Could we do that by allowing a subset of existing pseudo-classes on pseudo-elements. So something like html::view-transition-old(foo):only-child if the styles are conditional based on there being a pair of images vs an exit transition (only old content).

@tabatkins
Copy link
Member

Yes, the idea suggested by fantasai/miriam is that we expose the structure of the pseudo tree directly, with the names mapped to element names and the tags mapped to classes. So html::view-transition(.foo old) matching an "old" image who is a descendant of a "foo"-tagged container or image-group. Then you could write html::view-transition(old.foo:only-child) to get an old that doesn't have a new.

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Nov 3, 2022

I'll note that our suggestion doesn't rely specifically on the specific mapping of class/element syntax, but that provides a good example. Whatever the selector-syntax mapping (@FremyCompany's suggestion of IDs might make more sense), the goal is just to allow compound/complex selectors in the function.

One reason for that might be exposing the hierarchy. The more important goal in my mind was the ability to differentiate what happens in these three instances:

  1. There is only a new image.
  2. There is only an old image.
  3. There are both old and new images.

The solution mentioned a few times for that is using :only-child/:not(:only-child) on the element in question. I assume that solution is also possible with the current proposal?

html::view-transition-old(*):only-child {
  /* this is a default exit animation */
}

@jakearchibald
Copy link
Contributor Author

@mirisuzanne I think we need to make that work. And if it can't be :only-child, we should add something view-transition-specific to do the same thing.

@khushalsagar
Copy link
Member

I think we should be able to make the :only-child pseudo-class work. I figured pseudo-classes on pseudo-elements would already be a supported concept. For instance, before:hover seems like a valid use-case. But I just tried and it's not supported. Maybe there is an implementation complexity associated with supporting pseudo-classes on pseudo-elements in general that I'm missing.

@jakearchibald
Copy link
Contributor Author

I think it's tricky because what does ::before:only-child mean? We might need to allow-list which pseudos can use :only-child.

@khushalsagar
Copy link
Member

The spec edits for this issue have landed. I started a different one to capture the use-case discussed above.

In case I missed anything else on this issue which still needs discussion please file another issue for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests